opex792 commited on
Commit
8c3c2a2
·
verified ·
1 Parent(s): cd7b281

Upload 2 files

Browse files
Files changed (2) hide show
  1. database.js +2 -26
  2. server.js +13 -14
database.js CHANGED
@@ -4,16 +4,11 @@ import fetch from 'node-fetch';
4
  import { createGunzip } from 'zlib';
5
  import { createInterface } from 'readline';
6
 
7
- // --- Константы ---
8
  const DB_FILE = './movies_search.db';
9
  const DATA_URL = 'https://huggingface.co/datasets/opex792/kinopoisk/resolve/main/consolidated/kinopoisk.jsonl.gz?download=true';
10
 
11
  let db;
12
 
13
- /**
14
- * Инициализирует соединение с базой данных и создает необходимые таблицы,
15
- * включая FTS5-таблицу для полнотекстового поиска.
16
- */
17
  export async function initializeDatabase() {
18
  db = await open({ filename: DB_FILE, driver: sqlite3.Database });
19
  console.log('Подключение к SQLite (Search API) установлено.');
@@ -21,7 +16,6 @@ export async function initializeDatabase() {
21
  await db.exec('PRAGMA journal_mode = WAL;');
22
  await db.exec('PRAGMA synchronous = NORMAL;');
23
 
24
- // Таблица для хранения полных данных о фильмах
25
  await db.exec(`
26
  CREATE TABLE IF NOT EXISTS movies (
27
  id INTEGER PRIMARY KEY,
@@ -29,7 +23,6 @@ export async function initializeDatabase() {
29
  );
30
  `);
31
 
32
- // Виртуальная FTS5 таблица для эффективного полнотекстового поиска
33
  await db.exec(`
34
  CREATE VIRTUAL TABLE IF NOT EXISTS movie_names USING fts5(
35
  movie_id UNINDEXED,
@@ -39,13 +32,11 @@ export async function initializeDatabase() {
39
  `);
40
 
41
  console.log('Таблицы `movies` и `movie_names` (FTS5) готовы к работе.');
 
 
42
  return db;
43
  }
44
 
45
- /**
46
- * Полностью обновляет данные: скачивает, распаковывает и индексирует фильмы.
47
- * Работает в потоковом режиме, чтобы не потреблять много оперативной памяти.
48
- */
49
  export async function refreshData() {
50
  console.log('Начало полной загрузки и индексации данных...');
51
 
@@ -56,7 +47,6 @@ export async function refreshData() {
56
  response.body.pipe(gunzip);
57
  const rl = createInterface({ input: gunzip, crlfDelay: Infinity });
58
 
59
- // Очищаем таблицы перед новой загрузкой
60
  await db.exec('BEGIN TRANSACTION;');
61
  await db.exec('DELETE FROM movies;');
62
  await db.exec('DELETE FROM movie_names;');
@@ -72,10 +62,8 @@ export async function refreshData() {
72
  const movie = JSON.parse(line);
73
  if (!movie.id) continue;
74
 
75
- // 1. Сохраняем полный JSON объекта фильма
76
  await movieStmt.run(movie.id, JSON.stringify(movie));
77
 
78
- // 2. Индексируем все возможные названия для поиска
79
  const names = new Set();
80
  if (movie.name) names.add(movie.name);
81
  if (movie.alternativeName) names.add(movie.alternativeName);
@@ -99,15 +87,7 @@ export async function refreshData() {
99
  console.log(`✅ Процесс завершен. Всего загружено и проиндексировано ${processedCount} фильмов.`);
100
  }
101
 
102
- /**
103
- * Выполняет полнотекстовый поиск по проиндексированным названиям с пагинацией.
104
- * @param {string} queryString - Поисковый запрос.
105
- * @param {number} page - Номер страницы.
106
- * @param {number} limit - Количество результатов на странице.
107
- * @returns {Promise<{docs: Array<object>, total: number}>} Объект с результатами и общим количеством.
108
- */
109
  export async function searchByName(queryString, page = 1, limit = 10) {
110
- // Этап 1: Получаем общее количество уникальных фильмов, соответствующих запросу
111
  const countResult = await db.get(
112
  `SELECT count(DISTINCT movie_id) as total FROM movie_names WHERE movie_names MATCH ?`,
113
  [queryString]
@@ -118,7 +98,6 @@ export async function searchByName(queryString, page = 1, limit = 10) {
118
  return { docs: [], total: 0 };
119
  }
120
 
121
- // Этап 2: Получаем ID фильмов для текущей страницы, отсортированные по релевантности (rank)
122
  const offset = (page - 1) * limit;
123
  const searchResults = await db.all(
124
  `SELECT DISTINCT movie_id FROM movie_names WHERE movie_names MATCH ? ORDER BY rank BM25(1.0, 5.0) LIMIT ? OFFSET ?`,
@@ -129,17 +108,14 @@ export async function searchByName(queryString, page = 1, limit = 10) {
129
  return { docs: [], total };
130
  }
131
 
132
- // Этап 3: Извлекаем полные данные для найденных ID из нашей локальной таблицы `movies`
133
  const movieIds = searchResults.map(r => r.movie_id);
134
  const placeholders = movieIds.map(() => '?').join(',');
135
  const moviesData = await db.all(`SELECT data FROM movies WHERE id IN (${placeholders})`, movieIds);
136
 
137
- // Восстанавливаем порядок, заданный поиском
138
  const movieMap = new Map(moviesData.map(m => {
139
  const movieJson = JSON.parse(m.data);
140
  return [movieJson.id, movieJson];
141
  }));
142
-
143
  const docs = movieIds.map(id => movieMap.get(id));
144
 
145
  return { docs, total };
 
4
  import { createGunzip } from 'zlib';
5
  import { createInterface } from 'readline';
6
 
 
7
  const DB_FILE = './movies_search.db';
8
  const DATA_URL = 'https://huggingface.co/datasets/opex792/kinopoisk/resolve/main/consolidated/kinopoisk.jsonl.gz?download=true';
9
 
10
  let db;
11
 
 
 
 
 
12
  export async function initializeDatabase() {
13
  db = await open({ filename: DB_FILE, driver: sqlite3.Database });
14
  console.log('Подключение к SQLite (Search API) установлено.');
 
16
  await db.exec('PRAGMA journal_mode = WAL;');
17
  await db.exec('PRAGMA synchronous = NORMAL;');
18
 
 
19
  await db.exec(`
20
  CREATE TABLE IF NOT EXISTS movies (
21
  id INTEGER PRIMARY KEY,
 
23
  );
24
  `);
25
 
 
26
  await db.exec(`
27
  CREATE VIRTUAL TABLE IF NOT EXISTS movie_names USING fts5(
28
  movie_id UNINDEXED,
 
32
  `);
33
 
34
  console.log('Таблицы `movies` и `movie_names` (FTS5) готовы к работе.');
35
+
36
+ // ИСПРАВЛЕНИЕ: Возвращаем объект базы данных для использования в других модулях.
37
  return db;
38
  }
39
 
 
 
 
 
40
  export async function refreshData() {
41
  console.log('Начало полной загрузки и индексации данных...');
42
 
 
47
  response.body.pipe(gunzip);
48
  const rl = createInterface({ input: gunzip, crlfDelay: Infinity });
49
 
 
50
  await db.exec('BEGIN TRANSACTION;');
51
  await db.exec('DELETE FROM movies;');
52
  await db.exec('DELETE FROM movie_names;');
 
62
  const movie = JSON.parse(line);
63
  if (!movie.id) continue;
64
 
 
65
  await movieStmt.run(movie.id, JSON.stringify(movie));
66
 
 
67
  const names = new Set();
68
  if (movie.name) names.add(movie.name);
69
  if (movie.alternativeName) names.add(movie.alternativeName);
 
87
  console.log(`✅ Процесс завершен. Всего загружено и проиндексировано ${processedCount} фильмов.`);
88
  }
89
 
 
 
 
 
 
 
 
90
  export async function searchByName(queryString, page = 1, limit = 10) {
 
91
  const countResult = await db.get(
92
  `SELECT count(DISTINCT movie_id) as total FROM movie_names WHERE movie_names MATCH ?`,
93
  [queryString]
 
98
  return { docs: [], total: 0 };
99
  }
100
 
 
101
  const offset = (page - 1) * limit;
102
  const searchResults = await db.all(
103
  `SELECT DISTINCT movie_id FROM movie_names WHERE movie_names MATCH ? ORDER BY rank BM25(1.0, 5.0) LIMIT ? OFFSET ?`,
 
108
  return { docs: [], total };
109
  }
110
 
 
111
  const movieIds = searchResults.map(r => r.movie_id);
112
  const placeholders = movieIds.map(() => '?').join(',');
113
  const moviesData = await db.all(`SELECT data FROM movies WHERE id IN (${placeholders})`, movieIds);
114
 
 
115
  const movieMap = new Map(moviesData.map(m => {
116
  const movieJson = JSON.parse(m.data);
117
  return [movieJson.id, movieJson];
118
  }));
 
119
  const docs = movieIds.map(id => movieMap.get(id));
120
 
121
  return { docs, total };
server.js CHANGED
@@ -5,18 +5,17 @@ import { initializeDatabase, refreshData, searchByName } from './database.js';
5
 
6
  // --- Константы ---
7
  const PORT = process.env.PORT || 7860;
8
- const API_VERSION = "1.4"; // Версия API из документации
9
 
10
  // --- Настройка приложения Express ---
11
  const app = express();
12
  app.use(cors());
13
  app.use(express.json());
14
 
15
- let isReady = false; // Флаг готовности сервера к приему запросов
16
 
17
  // --- Middleware для проверки готовности ---
18
  app.use((req, res, next) => {
19
- // Пропускаем health-check, чтобы всегда можно было проверить состояние
20
  if (!isReady && req.path !== '/health') {
21
  return res.status(503).json({ message: 'Сервис временно недоступен, идет инициализация данных.', statusCode: 503 });
22
  }
@@ -28,7 +27,6 @@ app.get('/health', (req, res) => {
28
  res.status(200).json({ status: isReady ? 'UP' : 'DOWN', message: isReady ? 'Сервис готов к работе' : 'Идет инициализация' });
29
  });
30
 
31
- // Эндпоинт для поиска, полностью совместимый с документацией
32
  app.get(`/v${API_VERSION}/movie/search`, async (req, res) => {
33
  const { query, page, limit } = req.query;
34
 
@@ -37,15 +35,12 @@ app.get(`/v${API_VERSION}/movie/search`, async (req, res) => {
37
  }
38
 
39
  const pageNum = parseInt(page, 10) || 1;
40
- const limitNum = Math.min(parseInt(limit, 10) || 10, 250); // Лимит от 1 до 250
41
 
42
  try {
43
  const { docs, total } = await searchByName(query, pageNum, limitNum);
44
  const pages = Math.ceil(total / limitNum);
45
-
46
- // Формируем ответ в строгом соответствии со спецификацией
47
  res.json({ docs, total, limit: limitNum, page: pageNum, pages });
48
-
49
  } catch (e) {
50
  console.error(`Ошибка при поиске по запросу "${query}":`, e);
51
  res.status(500).json({ message: 'Внутренняя ошибка сервера при поиске.', error: e.message });
@@ -55,16 +50,21 @@ app.get(`/v${API_VERSION}/movie/search`, async (req, res) => {
55
  // --- Логика запуска сервера ---
56
  (async () => {
57
  try {
58
- await initializeDatabase();
 
59
 
60
  const needsRebuild = process.argv.includes('--rebuild-index');
61
- const count = await db.get('SELECT COUNT(*) as count FROM movies');
 
 
 
62
 
63
- // Запускаем полную перестройку индекса, если передан флаг или база пуста
64
- if (needsRebuild || count.count === 0) {
65
  if (needsRebuild) console.log('Обнаружен флаг --rebuild-index. Запуск принудительной переиндексации.');
66
- if (count.count === 0) console.log('База данных пуста. Запуск первоначальной загрузки.');
67
  await refreshData();
 
 
68
  }
69
 
70
  isReady = true;
@@ -75,7 +75,6 @@ app.get(`/v${API_VERSION}/movie/search`, async (req, res) => {
75
  console.log(`URL для поиска: http://localhost:${PORT}/v${API_VERSION}/movie/search`);
76
  });
77
 
78
- // Планируем автоматическое обновление данных каждый день в 3:00 ночи
79
  cron.schedule('0 3 * * *', async () => {
80
  console.log('Начало планового ежедневного обновления данных...');
81
  isReady = false;
 
5
 
6
  // --- Константы ---
7
  const PORT = process.env.PORT || 7860;
8
+ const API_VERSION = "1.4";
9
 
10
  // --- Настройка приложения Express ---
11
  const app = express();
12
  app.use(cors());
13
  app.use(express.json());
14
 
15
+ let isReady = false;
16
 
17
  // --- Middleware для проверки готовности ---
18
  app.use((req, res, next) => {
 
19
  if (!isReady && req.path !== '/health') {
20
  return res.status(503).json({ message: 'Сервис временно недоступен, идет инициализация данных.', statusCode: 503 });
21
  }
 
27
  res.status(200).json({ status: isReady ? 'UP' : 'DOWN', message: isReady ? 'Сервис готов к работе' : 'Идет инициализация' });
28
  });
29
 
 
30
  app.get(`/v${API_VERSION}/movie/search`, async (req, res) => {
31
  const { query, page, limit } = req.query;
32
 
 
35
  }
36
 
37
  const pageNum = parseInt(page, 10) || 1;
38
+ const limitNum = Math.min(parseInt(limit, 10) || 10, 250);
39
 
40
  try {
41
  const { docs, total } = await searchByName(query, pageNum, limitNum);
42
  const pages = Math.ceil(total / limitNum);
 
 
43
  res.json({ docs, total, limit: limitNum, page: pageNum, pages });
 
44
  } catch (e) {
45
  console.error(`Ошибка при поиске по запросу "${query}":`, e);
46
  res.status(500).json({ message: 'Внутренняя ошибка сервера при поиске.', error: e.message });
 
50
  // --- Логика запуска сервера ---
51
  (async () => {
52
  try {
53
+ // ИСПРАВЛЕНИЕ: Получаем объект 'db' после инициализации.
54
+ const db = await initializeDatabase();
55
 
56
  const needsRebuild = process.argv.includes('--rebuild-index');
57
+
58
+ // ИСПРАВЛЕНИЕ: Используем полученный 'db' для запроса.
59
+ const result = await db.get('SELECT COUNT(*) as count FROM movies');
60
+ const moviesCount = result.count;
61
 
62
+ if (needsRebuild || moviesCount === 0) {
 
63
  if (needsRebuild) console.log('Обнаружен флаг --rebuild-index. Запуск принудительной переиндексации.');
64
+ if (moviesCount === 0) console.log('База данных пуста. Запуск первоначальной загрузки.');
65
  await refreshData();
66
+ } else {
67
+ console.log(`В базе уже есть ${moviesCount} фильмов. Запуск в штатном режиме.`);
68
  }
69
 
70
  isReady = true;
 
75
  console.log(`URL для поиска: http://localhost:${PORT}/v${API_VERSION}/movie/search`);
76
  });
77
 
 
78
  cron.schedule('0 3 * * *', async () => {
79
  console.log('Начало планового ежедневного обновления данных...');
80
  isReady = false;