Update app.py
Browse files
app.py
CHANGED
@@ -40,29 +40,19 @@ logging.info("Модель загружена успешно.")
|
|
40 |
|
41 |
# Jina AI Reranker API
|
42 |
JINA_API_URL = 'https://api.jina.ai/v1/rerank'
|
43 |
-
JINA_API_KEY = os.environ.get("JINA_API_KEY")
|
44 |
if JINA_API_KEY is None:
|
45 |
raise ValueError("JINA_API_KEY environment variable not set.")
|
46 |
JINA_RERANKER_MODEL = "jina-reranker-v2-base-multilingual"
|
47 |
|
48 |
-
|
49 |
# Имена таблиц
|
50 |
embeddings_table = "movie_embeddings"
|
51 |
query_cache_table = "query_cache"
|
|
|
52 |
|
53 |
# Максимальный размер таблицы кэша запросов в байтах (50MB)
|
54 |
MAX_CACHE_SIZE = 50 * 1024 * 1024
|
55 |
|
56 |
-
# Загружаем данные из файла movies.json
|
57 |
-
try:
|
58 |
-
import json
|
59 |
-
with open("movies.json", "r", encoding="utf-8") as f:
|
60 |
-
movies_data = json.load(f)
|
61 |
-
logging.info(f"Загружено {len(movies_data)} фильмов из movies.json")
|
62 |
-
except FileNotFoundError:
|
63 |
-
logging.error("Ошибка: Файл movies.json не найден.")
|
64 |
-
movies_data = []
|
65 |
-
|
66 |
# Очередь для необработанных фильмов
|
67 |
movies_queue = queue.Queue()
|
68 |
|
@@ -101,32 +91,29 @@ def setup_database():
|
|
101 |
# Создаем расширение pgvector если его нет
|
102 |
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
|
103 |
|
104 |
-
# Удаляем существующие таблицы если они есть
|
105 |
-
# cur.execute(f"DROP TABLE IF EXISTS {embeddings_table}, {query_cache_table};")
|
106 |
-
|
107 |
# Создаем таблицу для хранения эмбеддингов фильмов
|
108 |
cur.execute(f"""
|
109 |
-
CREATE TABLE {embeddings_table} (
|
110 |
movie_id INTEGER PRIMARY KEY,
|
111 |
embedding_crc32 BIGINT,
|
112 |
string_crc32 BIGINT,
|
113 |
model_name TEXT,
|
114 |
embedding vector(1024)
|
115 |
);
|
116 |
-
CREATE INDEX ON {embeddings_table} (string_crc32);
|
117 |
""")
|
118 |
|
119 |
# Создаем таблицу для кэширования запросов
|
120 |
cur.execute(f"""
|
121 |
-
CREATE TABLE {query_cache_table} (
|
122 |
query_crc32 BIGINT PRIMARY KEY,
|
123 |
query TEXT,
|
124 |
model_name TEXT,
|
125 |
embedding vector(1024),
|
126 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
127 |
);
|
128 |
-
CREATE INDEX ON {query_cache_table} (query_crc32);
|
129 |
-
CREATE INDEX ON {query_cache_table} (created_at);
|
130 |
""")
|
131 |
|
132 |
conn.commit()
|
@@ -158,14 +145,26 @@ def get_movies_without_embeddings():
|
|
158 |
movies_to_process = []
|
159 |
try:
|
160 |
with conn.cursor() as cur:
|
161 |
-
# Получаем список ID фильмов, которые уже есть в
|
162 |
cur.execute(f"SELECT movie_id FROM {embeddings_table}")
|
163 |
existing_ids = {row[0] for row in cur.fetchall()}
|
164 |
|
165 |
-
#
|
166 |
-
|
167 |
-
|
168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
|
170 |
logging.info(f"Найдено {len(movies_to_process)} фильмов для об��аботки.")
|
171 |
except Exception as e:
|
@@ -215,23 +214,22 @@ def process_batch(batch):
|
|
215 |
return
|
216 |
|
217 |
try:
|
218 |
-
for
|
219 |
-
|
220 |
-
string_crc32 = calculate_crc32(embedding_string)
|
221 |
|
222 |
# Проверяем существующий эмбеддинг
|
223 |
existing_embedding = get_embedding_from_db(conn, embeddings_table, "string_crc32", string_crc32, model_name)
|
224 |
|
225 |
if existing_embedding is None:
|
226 |
-
embedding = encode_string(
|
227 |
embedding_crc32 = calculate_crc32(str(embedding.tolist()))
|
228 |
|
229 |
-
if insert_embedding(conn, embeddings_table,
|
230 |
-
logging.info(f"Сохранен эмбеддинг для '{
|
231 |
else:
|
232 |
-
logging.error(f"Ошибка сохранения эмбеддинга для '{
|
233 |
else:
|
234 |
-
logging.info(f"Эмбеддинг для '{
|
235 |
except Exception as e:
|
236 |
logging.error(f"Ошибка при обработке пакета фильмов: {e}")
|
237 |
finally:
|
@@ -281,32 +279,44 @@ def process_movies():
|
|
281 |
processing_complete = True
|
282 |
logging.info("Обработка фильмов завершена")
|
283 |
|
284 |
-
def
|
285 |
-
"""
|
286 |
-
|
287 |
try:
|
288 |
with conn.cursor() as cur:
|
289 |
-
cur.execute(f"
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
|
|
|
|
|
|
|
|
297 |
except Exception as e:
|
298 |
-
logging.error(f"Ошибка при
|
299 |
-
return
|
300 |
|
301 |
def rerank_with_api(query, results, top_k):
|
302 |
"""Переранжирует результаты с помощью Jina AI Reranker API."""
|
303 |
logging.info(f"Начало переранжирования для запроса: '{query}'")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
304 |
documents = []
|
305 |
for movie_id, _ in results:
|
306 |
-
|
307 |
-
if
|
308 |
-
|
309 |
-
|
|
|
310 |
|
311 |
headers = {
|
312 |
'Content-Type': 'application/json',
|
@@ -318,18 +328,17 @@ def rerank_with_api(query, results, top_k):
|
|
318 |
"top_n": top_k,
|
319 |
"documents": documents
|
320 |
}
|
321 |
-
logging.info(f"Отправка данных на реранжировку (
|
322 |
|
323 |
try:
|
324 |
response = requests.post(JINA_API_URL, headers=headers, json=data)
|
325 |
-
response.raise_for_status()
|
326 |
result = response.json()
|
327 |
-
logging.info(f"Ответ от API реранжировщика
|
328 |
|
329 |
reranked_results = []
|
330 |
if 'results' in result:
|
331 |
for item in result['results']:
|
332 |
-
|
333 |
index = item['index']
|
334 |
movie_id = results[index][0]
|
335 |
reranked_results.append((movie_id, item['relevance_score']))
|
@@ -337,7 +346,6 @@ def rerank_with_api(query, results, top_k):
|
|
337 |
logging.warning("Ответ от API не содержит ключа 'results'.")
|
338 |
|
339 |
logging.info("Переранжирование завершено.")
|
340 |
-
# time.sleep(0.1)
|
341 |
return reranked_results
|
342 |
|
343 |
except requests.exceptions.RequestException as e:
|
@@ -394,20 +402,29 @@ def search_movies(query, top_k=25):
|
|
394 |
except Exception as e:
|
395 |
logging.error(f"Ошибка при выполнении поискового запроса: {e}")
|
396 |
results = []
|
|
|
|
|
397 |
|
398 |
# Переранжируем результаты с помощью API
|
399 |
reranked_results = rerank_with_api(query, results, top_k)
|
400 |
|
|
|
|
|
|
|
|
|
|
|
401 |
output = ""
|
402 |
for movie_id, score in reranked_results:
|
403 |
-
# Находим
|
404 |
-
|
405 |
-
if
|
406 |
-
output += f"<h3>{
|
407 |
-
output += f"<p><strong>Жанры:</strong> {
|
408 |
-
output += f"<p><strong>Описание:</strong> {
|
409 |
output += f"<p><strong>Релевантность (reranker score):</strong> {score:.4f}</p>\n"
|
410 |
output += "<hr>\n"
|
|
|
|
|
411 |
|
412 |
search_time = time.time() - start_time
|
413 |
logging.info(f"Поиск выполнен за {search_time:.2f} секунд.")
|
@@ -419,8 +436,6 @@ def search_movies(query, top_k=25):
|
|
419 |
return "<p>Произошла ошибка при выполнении поиска.</p>"
|
420 |
|
421 |
finally:
|
422 |
-
if conn:
|
423 |
-
conn.close()
|
424 |
search_in_progress = False
|
425 |
|
426 |
# Запускаем обработку фильмов в отдельном потоке
|
|
|
40 |
|
41 |
# Jina AI Reranker API
|
42 |
JINA_API_URL = 'https://api.jina.ai/v1/rerank'
|
43 |
+
JINA_API_KEY = os.environ.get("JINA_API_KEY")
|
44 |
if JINA_API_KEY is None:
|
45 |
raise ValueError("JINA_API_KEY environment variable not set.")
|
46 |
JINA_RERANKER_MODEL = "jina-reranker-v2-base-multilingual"
|
47 |
|
|
|
48 |
# Имена таблиц
|
49 |
embeddings_table = "movie_embeddings"
|
50 |
query_cache_table = "query_cache"
|
51 |
+
movies_table = "Movies" # Имя таблицы с фильмами
|
52 |
|
53 |
# Максимальный размер таблицы кэша запросов в байтах (50MB)
|
54 |
MAX_CACHE_SIZE = 50 * 1024 * 1024
|
55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
# Очередь для необработанных фильмов
|
57 |
movies_queue = queue.Queue()
|
58 |
|
|
|
91 |
# Создаем расширение pgvector если его нет
|
92 |
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
|
93 |
|
|
|
|
|
|
|
94 |
# Создаем таблицу для хранения эмбеддингов фильмов
|
95 |
cur.execute(f"""
|
96 |
+
CREATE TABLE IF NOT EXISTS {embeddings_table} (
|
97 |
movie_id INTEGER PRIMARY KEY,
|
98 |
embedding_crc32 BIGINT,
|
99 |
string_crc32 BIGINT,
|
100 |
model_name TEXT,
|
101 |
embedding vector(1024)
|
102 |
);
|
103 |
+
CREATE INDEX IF NOT EXISTS idx_string_crc32 ON {embeddings_table} (string_crc32);
|
104 |
""")
|
105 |
|
106 |
# Создаем таблицу для кэширования запросов
|
107 |
cur.execute(f"""
|
108 |
+
CREATE TABLE IF NOT EXISTS {query_cache_table} (
|
109 |
query_crc32 BIGINT PRIMARY KEY,
|
110 |
query TEXT,
|
111 |
model_name TEXT,
|
112 |
embedding vector(1024),
|
113 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
114 |
);
|
115 |
+
CREATE INDEX IF NOT EXISTS idx_query_crc32 ON {query_cache_table} (query_crc32);
|
116 |
+
CREATE INDEX IF NOT EXISTS idx_created_at ON {query_cache_table} (created_at);
|
117 |
""")
|
118 |
|
119 |
conn.commit()
|
|
|
145 |
movies_to_process = []
|
146 |
try:
|
147 |
with conn.cursor() as cur:
|
148 |
+
# Получаем список ID фильмов, которые уже есть в таблице эмбеддингов
|
149 |
cur.execute(f"SELECT movie_id FROM {embeddings_table}")
|
150 |
existing_ids = {row[0] for row in cur.fetchall()}
|
151 |
|
152 |
+
# Получаем список всех фильмов из таблицы Movies с подготовленной строкой
|
153 |
+
cur.execute(f"""
|
154 |
+
SELECT id, data,
|
155 |
+
'Название: ' || data->>'name' ||
|
156 |
+
'\\nГод: ' || data->>'year' ||
|
157 |
+
'\\nЖанры: ' || (SELECT string_agg(genre->>'name', ', ') FROM jsonb_array_elements(data->'genres') AS genre) ||
|
158 |
+
'\\nОписание: ' || COALESCE(data->>'description', '')
|
159 |
+
AS prepared_string
|
160 |
+
FROM {movies_table}
|
161 |
+
""")
|
162 |
+
all_movies = cur.fetchall()
|
163 |
+
|
164 |
+
# Фильтруем только те фильмы, которых нет в таблице эмбеддингов
|
165 |
+
for movie_id, movie_data, prepared_string in all_movies:
|
166 |
+
if movie_id not in existing_ids:
|
167 |
+
movies_to_process.append((movie_id, movie_data, prepared_string))
|
168 |
|
169 |
logging.info(f"Найдено {len(movies_to_process)} фильмов для об��аботки.")
|
170 |
except Exception as e:
|
|
|
214 |
return
|
215 |
|
216 |
try:
|
217 |
+
for movie_id, movie_data, prepared_string in batch:
|
218 |
+
string_crc32 = calculate_crc32(prepared_string)
|
|
|
219 |
|
220 |
# Проверяем существующий эмбеддинг
|
221 |
existing_embedding = get_embedding_from_db(conn, embeddings_table, "string_crc32", string_crc32, model_name)
|
222 |
|
223 |
if existing_embedding is None:
|
224 |
+
embedding = encode_string(prepared_string)
|
225 |
embedding_crc32 = calculate_crc32(str(embedding.tolist()))
|
226 |
|
227 |
+
if insert_embedding(conn, embeddings_table, movie_id, embedding_crc32, string_crc32, embedding):
|
228 |
+
logging.info(f"Сохранен эмбеддинг для '{movie_data['name']}' (ID: {movie_id})")
|
229 |
else:
|
230 |
+
logging.error(f"Ошибка сохранения эмбеддинга для '{movie_data['name']}' (ID: {movie_id})")
|
231 |
else:
|
232 |
+
logging.info(f"Эмбеддинг для '{movie_data['name']}' (ID: {movie_id}) уже существует")
|
233 |
except Exception as e:
|
234 |
logging.error(f"Ошибка при обработке пакета фильмов: {e}")
|
235 |
finally:
|
|
|
279 |
processing_complete = True
|
280 |
logging.info("Обработка фильмов завершена")
|
281 |
|
282 |
+
def get_movie_data_from_db(conn, movie_ids):
|
283 |
+
"""Получает данные фильмов из таблицы Movies по списку ID."""
|
284 |
+
movie_data_dict = {}
|
285 |
try:
|
286 |
with conn.cursor() as cur:
|
287 |
+
cur.execute(f"""
|
288 |
+
SELECT id, data,
|
289 |
+
'Название: ' || data->>'name' ||
|
290 |
+
'\\nГод: ' || data->>'year' ||
|
291 |
+
'\\nЖанры: ' || (SELECT string_agg(genre->>'name', ', ') FROM jsonb_array_elements(data->'genres') AS genre) ||
|
292 |
+
'\\nОписание: ' || COALESCE(data->>'description', '')
|
293 |
+
AS prepared_string
|
294 |
+
FROM {movies_table}
|
295 |
+
WHERE id IN %s
|
296 |
+
""", (tuple(movie_ids),))
|
297 |
+
for movie_id, movie_data, prepared_string in cur.fetchall():
|
298 |
+
movie_data_dict[movie_id] = (movie_data, prepared_string)
|
299 |
except Exception as e:
|
300 |
+
logging.error(f"Ошибка при получении данных фильмов из БД: {e}")
|
301 |
+
return movie_data_dict
|
302 |
|
303 |
def rerank_with_api(query, results, top_k):
|
304 |
"""Переранжирует результаты с помощью Jina AI Reranker API."""
|
305 |
logging.info(f"Начало переранжирования для запроса: '{query}'")
|
306 |
+
|
307 |
+
# Получаем данные фильмов из БД
|
308 |
+
conn = get_db_connection()
|
309 |
+
movie_ids = [movie_id for movie_id, _ in results]
|
310 |
+
movie_data_dict = get_movie_data_from_db(conn, movie_ids)
|
311 |
+
conn.close()
|
312 |
+
|
313 |
documents = []
|
314 |
for movie_id, _ in results:
|
315 |
+
movie_data, prepared_string = movie_data_dict.get(movie_id, (None, None))
|
316 |
+
if movie_data:
|
317 |
+
documents.append(prepared_string)
|
318 |
+
else:
|
319 |
+
logging.warning(f"Данные для фильма с ID {movie_id} не найдены в БД.")
|
320 |
|
321 |
headers = {
|
322 |
'Content-Type': 'application/json',
|
|
|
328 |
"top_n": top_k,
|
329 |
"documents": documents
|
330 |
}
|
331 |
+
logging.info(f"Отправка данных на реранжировку (documents count): {len(data['documents'])}")
|
332 |
|
333 |
try:
|
334 |
response = requests.post(JINA_API_URL, headers=headers, json=data)
|
335 |
+
response.raise_for_status()
|
336 |
result = response.json()
|
337 |
+
logging.info(f"Ответ от API реранжировщика получен.")
|
338 |
|
339 |
reranked_results = []
|
340 |
if 'results' in result:
|
341 |
for item in result['results']:
|
|
|
342 |
index = item['index']
|
343 |
movie_id = results[index][0]
|
344 |
reranked_results.append((movie_id, item['relevance_score']))
|
|
|
346 |
logging.warning("Ответ от API не содержит ключа 'results'.")
|
347 |
|
348 |
logging.info("Переранжирование завершено.")
|
|
|
349 |
return reranked_results
|
350 |
|
351 |
except requests.exceptions.RequestException as e:
|
|
|
402 |
except Exception as e:
|
403 |
logging.error(f"Ошибка при выполнении поискового запроса: {e}")
|
404 |
results = []
|
405 |
+
finally:
|
406 |
+
conn.close()
|
407 |
|
408 |
# Переранжируем результаты с помощью API
|
409 |
reranked_results = rerank_with_api(query, results, top_k)
|
410 |
|
411 |
+
conn = get_db_connection()
|
412 |
+
movie_ids = [movie_id for movie_id, _ in reranked_results]
|
413 |
+
movie_data_dict = get_movie_data_from_db(conn, movie_ids)
|
414 |
+
conn.close()
|
415 |
+
|
416 |
output = ""
|
417 |
for movie_id, score in reranked_results:
|
418 |
+
# Находим данные фильма
|
419 |
+
movie_data, _ = movie_data_dict.get(movie_id, (None, None))
|
420 |
+
if movie_data:
|
421 |
+
output += f"<h3>{movie_data['name']} ({movie_data['year']})</h3>\n"
|
422 |
+
output += f"<p><strong>Жанры:</strong> {', '.join([genre['name'] for genre in movie_data['genres']])}</p>\n"
|
423 |
+
output += f"<p><strong>Описание:</strong> {movie_data.get('description', '')}</p>\n"
|
424 |
output += f"<p><strong>Релевантность (reranker score):</strong> {score:.4f}</p>\n"
|
425 |
output += "<hr>\n"
|
426 |
+
else:
|
427 |
+
logging.warning(f"Данные для фильма с ID {movie_id} не найдены в БД.")
|
428 |
|
429 |
search_time = time.time() - start_time
|
430 |
logging.info(f"Поиск выполнен за {search_time:.2f} секунд.")
|
|
|
436 |
return "<p>Произошла ошибка при выполнении поиска.</p>"
|
437 |
|
438 |
finally:
|
|
|
|
|
439 |
search_in_progress = False
|
440 |
|
441 |
# Запускаем обработку фильмов в отдельном потоке
|