Update app.py
Browse files
app.py
CHANGED
@@ -51,6 +51,7 @@ for movie in movies_data:
|
|
51 |
|
52 |
# Флаг, указывающий, что обработка фильмов завершена
|
53 |
processing_complete = False
|
|
|
54 |
# Флаг, указывающий, что выполняется поиск
|
55 |
search_in_progress = False
|
56 |
|
@@ -74,11 +75,11 @@ def setup_database():
|
|
74 |
conn = get_db_connection()
|
75 |
if conn is None:
|
76 |
return
|
77 |
-
|
78 |
with conn.cursor() as cur:
|
79 |
# Создаем расширение pgvector
|
80 |
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
|
81 |
-
|
82 |
# Создаем таблицу для хранения эмбеддингов фильмов
|
83 |
cur.execute(f"""
|
84 |
CREATE TABLE IF NOT EXISTS {embeddings_table} (
|
@@ -89,7 +90,7 @@ def setup_database():
|
|
89 |
embedding vector(1024)
|
90 |
);
|
91 |
""")
|
92 |
-
|
93 |
# Создаем таблицу для кэширования эмбеддингов запросов
|
94 |
cur.execute(f"""
|
95 |
CREATE TABLE IF NOT EXISTS {query_cache_table} (
|
@@ -102,7 +103,7 @@ def setup_database():
|
|
102 |
CREATE INDEX IF NOT EXISTS idx_query_crc32 ON {query_cache_table} (query_crc32);
|
103 |
CREATE INDEX IF NOT EXISTS idx_created_at ON {query_cache_table} (created_at);
|
104 |
""")
|
105 |
-
|
106 |
# Создаем функцию и триггер для автоматического удаления старых записей из таблицы кэша запросов
|
107 |
cur.execute(f"""
|
108 |
CREATE OR REPLACE FUNCTION manage_query_cache_size()
|
@@ -113,11 +114,7 @@ def setup_database():
|
|
113 |
BEGIN
|
114 |
SELECT pg_total_relation_size('{query_cache_table}') INTO table_size;
|
115 |
IF table_size > {MAX_CACHE_SIZE} THEN
|
116 |
-
FOR row_to_delete IN
|
117 |
-
SELECT query_crc32
|
118 |
-
FROM {query_cache_table}
|
119 |
-
ORDER BY created_at ASC
|
120 |
-
LOOP
|
121 |
DELETE FROM {query_cache_table} WHERE query_crc32 = row_to_delete.query_crc32;
|
122 |
SELECT pg_total_relation_size('{query_cache_table}') INTO table_size;
|
123 |
EXIT WHEN table_size <= {MAX_CACHE_SIZE};
|
@@ -132,7 +129,8 @@ def setup_database():
|
|
132 |
FOR EACH ROW
|
133 |
EXECUTE PROCEDURE manage_query_cache_size();
|
134 |
""")
|
135 |
-
|
|
|
136 |
conn.close()
|
137 |
|
138 |
# Настраиваем базу данных при запуске приложения
|
@@ -164,14 +162,14 @@ def insert_embedding(conn, table_name, crc32_column, crc32_value, other_columns,
|
|
164 |
columns = ', '.join([crc32_column] + list(other_columns.keys()) + ['model_name', 'embedding'])
|
165 |
placeholders = ', '.join(['%s'] * (len(other_columns) + 3))
|
166 |
values = (crc32_value,) + tuple(other_columns.values()) + (model_name, embedding.tolist())
|
167 |
-
|
168 |
with conn.cursor() as cur:
|
169 |
try:
|
170 |
cur.execute(f"""
|
171 |
INSERT INTO {table_name} ({columns})
|
172 |
VALUES ({placeholders})
|
173 |
ON CONFLICT ({crc32_column}) DO NOTHING;
|
174 |
-
|
175 |
conn.commit()
|
176 |
return True
|
177 |
except Exception as e:
|
@@ -212,22 +210,22 @@ def process_movies():
|
|
212 |
f"Название: {movie['name']}\nГод: {movie['year']}\nЖанры: {movie['genresList']}\nОписание: {movie['description']}"
|
213 |
for movie in batch
|
214 |
]
|
215 |
-
|
216 |
print(f"Создаются эмбеддинги для фильмов: {', '.join(titles)}...")
|
217 |
-
|
218 |
with db_lock:
|
219 |
for movie, embedding_string in zip(batch, embedding_strings):
|
220 |
movie_id = movie['id']
|
221 |
string_crc32 = calculate_crc32(embedding_string)
|
222 |
-
|
223 |
# Проверяем, есть ли уже эмбеддинг для этого фильма в базе данных
|
224 |
existing_embedding = get_embedding_from_db(conn, embeddings_table, "string_crc32", string_crc32, model_name)
|
225 |
-
|
226 |
if existing_embedding is None:
|
227 |
# Создаем эмбеддинг, только если его нет в базе данных
|
228 |
embedding = encode_string(embedding_string)
|
229 |
-
embedding_crc32 = calculate_crc32(embedding.numpy().tobytes())
|
230 |
-
|
231 |
if insert_embedding(conn, embeddings_table, "embedding_crc32", embedding_crc32, {"movie_id": movie_id, "string_crc32": string_crc32}, embedding):
|
232 |
print(f"Эмбеддинг для фильма '{movie['name']}' сохранен в базе данных.")
|
233 |
else:
|
@@ -237,7 +235,7 @@ def process_movies():
|
|
237 |
|
238 |
conn.close()
|
239 |
print("Обработка фильмов завершена.")
|
240 |
-
|
241 |
def get_movie_embeddings(conn):
|
242 |
"""Загружает все эмбеддинги фильмов из базы данных."""
|
243 |
movie_embeddings = {}
|
@@ -257,101 +255,83 @@ def get_movie_embeddings(conn):
|
|
257 |
def search_movies(query, top_k=10):
|
258 |
"""
|
259 |
Ищет наиболее похожие фильмы по запросу.
|
260 |
-
|
261 |
Args:
|
262 |
query: Текстовый запрос.
|
263 |
top_k: Количество возвращаемых результатов.
|
264 |
-
|
265 |
Returns:
|
266 |
Строку с результатами поиска в формате HTML.
|
267 |
"""
|
268 |
global search_in_progress
|
269 |
search_in_progress = True
|
270 |
start_time = time.time()
|
|
|
271 |
print(f"\n\033[1mПоиск по запросу: '{query}'\033[0m")
|
272 |
-
|
273 |
conn = get_db_connection()
|
274 |
if conn is None:
|
275 |
search_in_progress = False
|
276 |
return "<p>Ошибка подключения к базе данных.</p>"
|
277 |
|
278 |
query_crc32 = calculate_crc32(query)
|
279 |
-
|
280 |
# Проверяем, есть ли уже эмбеддинг для этого запроса в кэше
|
281 |
print(f"Начало поиска эмбеддинга запроса в кэше: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
282 |
query_embedding_tensor = get_embedding_from_db(conn, query_cache_table, "query_crc32", query_crc32, model_name)
|
283 |
print(f"Окончание поиска эмбеддинга запроса в кэше: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
284 |
|
285 |
if query_embedding_tensor is None:
|
286 |
-
|
|
|
287 |
query_embedding_tensor = encode_string(query)
|
288 |
-
print(f"Окончание создания эмбеддинга
|
289 |
-
|
290 |
-
#
|
291 |
insert_embedding(conn, query_cache_table, "query_crc32", query_crc32, {"query": query}, query_embedding_tensor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
292 |
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
if not current_movie_embeddings:
|
299 |
-
search_in_progress = False
|
300 |
-
return "<p>Пока что нет обработанных фильмов. Попробуйте позже.</p>"
|
301 |
-
|
302 |
-
# Преобразуем эмбеддинги фильмов в тензор
|
303 |
-
movie_titles = list(current_movie_embeddings.keys())
|
304 |
-
movie_embeddings_tensor = torch.stack(list(current_movie_embeddings.values()))
|
305 |
-
|
306 |
-
print(f"Начало поиска похожих фильмов: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
307 |
-
# Используем util.semantic_search для поиска похожих фильмов
|
308 |
-
hits = util.semantic_search(query_embedding_tensor, movie_embeddings_tensor, top_k=top_k)[0]
|
309 |
-
print(f"Окончание поиска похожих фильмов: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
310 |
-
|
311 |
-
results_html = ""
|
312 |
-
for hit in hits:
|
313 |
-
title = movie_titles[hit['corpus_id']]
|
314 |
-
score = hit['score']
|
315 |
-
# Ищем полное описание фильма в исходных данных
|
316 |
-
for movie in movies_data:
|
317 |
-
if movie["name"] == title:
|
318 |
-
description = movie["description"]
|
319 |
-
year = movie["year"]
|
320 |
-
genres = movie["genresList"]
|
321 |
-
break
|
322 |
-
|
323 |
-
results_html += f"<h3><b>{title} ({year})</b></h3>"
|
324 |
-
results_html += f"<p><b>Жанры:</b> {genres}</p>"
|
325 |
-
results_html += f"<p><b>Описание:</b> {description}</p>"
|
326 |
-
results_html += f"<p><b>Сходство:</b> {score:.4f}</p>"
|
327 |
-
results_html += "<hr>"
|
328 |
|
329 |
-
end_time = time.time()
|
330 |
-
execution_time = end_time - start_time
|
331 |
-
print(f"Поиск завершен за {execution_time:.4f} секунд.")
|
332 |
search_in_progress = False
|
333 |
-
|
|
|
|
|
334 |
|
335 |
-
|
|
|
|
|
336 |
processing_thread = threading.Thread(target=process_movies)
|
|
|
337 |
|
338 |
# Создаем интерфейс Gradio
|
339 |
iface = gr.Interface(
|
340 |
fn=search_movies,
|
341 |
-
inputs=gr.Textbox(
|
342 |
-
outputs=gr.HTML(label="Результаты
|
343 |
-
title="
|
344 |
-
description="Введите
|
345 |
-
examples=[
|
346 |
-
["Фильм про ограбление"],
|
347 |
-
["Комедия 2019 года"],
|
348 |
-
["Фантастика про космос"],
|
349 |
-
],
|
350 |
)
|
351 |
|
352 |
-
# Запускаем
|
353 |
-
|
354 |
-
|
355 |
-
# Запускаем приложение
|
356 |
-
iface.queue()
|
357 |
-
iface.launch()
|
|
|
51 |
|
52 |
# Флаг, указывающий, что обработка фильмов завершена
|
53 |
processing_complete = False
|
54 |
+
|
55 |
# Флаг, указывающий, что выполняется поиск
|
56 |
search_in_progress = False
|
57 |
|
|
|
75 |
conn = get_db_connection()
|
76 |
if conn is None:
|
77 |
return
|
78 |
+
|
79 |
with conn.cursor() as cur:
|
80 |
# Создаем расширение pgvector
|
81 |
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
|
82 |
+
|
83 |
# Создаем таблицу для хранения эмбеддингов фильмов
|
84 |
cur.execute(f"""
|
85 |
CREATE TABLE IF NOT EXISTS {embeddings_table} (
|
|
|
90 |
embedding vector(1024)
|
91 |
);
|
92 |
""")
|
93 |
+
|
94 |
# Создаем таблицу для кэширования эмбеддингов запросов
|
95 |
cur.execute(f"""
|
96 |
CREATE TABLE IF NOT EXISTS {query_cache_table} (
|
|
|
103 |
CREATE INDEX IF NOT EXISTS idx_query_crc32 ON {query_cache_table} (query_crc32);
|
104 |
CREATE INDEX IF NOT EXISTS idx_created_at ON {query_cache_table} (created_at);
|
105 |
""")
|
106 |
+
|
107 |
# Создаем функцию и триггер для автоматического удаления старых записей из таблицы кэша запросов
|
108 |
cur.execute(f"""
|
109 |
CREATE OR REPLACE FUNCTION manage_query_cache_size()
|
|
|
114 |
BEGIN
|
115 |
SELECT pg_total_relation_size('{query_cache_table}') INTO table_size;
|
116 |
IF table_size > {MAX_CACHE_SIZE} THEN
|
117 |
+
FOR row_to_delete IN SELECT query_crc32 FROM {query_cache_table} ORDER BY created_at ASC LOOP
|
|
|
|
|
|
|
|
|
118 |
DELETE FROM {query_cache_table} WHERE query_crc32 = row_to_delete.query_crc32;
|
119 |
SELECT pg_total_relation_size('{query_cache_table}') INTO table_size;
|
120 |
EXIT WHEN table_size <= {MAX_CACHE_SIZE};
|
|
|
129 |
FOR EACH ROW
|
130 |
EXECUTE PROCEDURE manage_query_cache_size();
|
131 |
""")
|
132 |
+
|
133 |
+
conn.commit()
|
134 |
conn.close()
|
135 |
|
136 |
# Настраиваем базу данных при запуске приложения
|
|
|
162 |
columns = ', '.join([crc32_column] + list(other_columns.keys()) + ['model_name', 'embedding'])
|
163 |
placeholders = ', '.join(['%s'] * (len(other_columns) + 3))
|
164 |
values = (crc32_value,) + tuple(other_columns.values()) + (model_name, embedding.tolist())
|
165 |
+
|
166 |
with conn.cursor() as cur:
|
167 |
try:
|
168 |
cur.execute(f"""
|
169 |
INSERT INTO {table_name} ({columns})
|
170 |
VALUES ({placeholders})
|
171 |
ON CONFLICT ({crc32_column}) DO NOTHING;
|
172 |
+
""", values)
|
173 |
conn.commit()
|
174 |
return True
|
175 |
except Exception as e:
|
|
|
210 |
f"Название: {movie['name']}\nГод: {movie['year']}\nЖанры: {movie['genresList']}\nОписание: {movie['description']}"
|
211 |
for movie in batch
|
212 |
]
|
213 |
+
|
214 |
print(f"Создаются эмбеддинги для фильмов: {', '.join(titles)}...")
|
215 |
+
|
216 |
with db_lock:
|
217 |
for movie, embedding_string in zip(batch, embedding_strings):
|
218 |
movie_id = movie['id']
|
219 |
string_crc32 = calculate_crc32(embedding_string)
|
220 |
+
|
221 |
# Проверяем, есть ли уже эмбеддинг для этого фильма в базе данных
|
222 |
existing_embedding = get_embedding_from_db(conn, embeddings_table, "string_crc32", string_crc32, model_name)
|
223 |
+
|
224 |
if existing_embedding is None:
|
225 |
# Создаем эмбеддинг, только если его нет в базе данных
|
226 |
embedding = encode_string(embedding_string)
|
227 |
+
embedding_crc32 = calculate_crc32(embedding.cpu().numpy().tobytes())
|
228 |
+
|
229 |
if insert_embedding(conn, embeddings_table, "embedding_crc32", embedding_crc32, {"movie_id": movie_id, "string_crc32": string_crc32}, embedding):
|
230 |
print(f"Эмбеддинг для фильма '{movie['name']}' сохранен в базе данных.")
|
231 |
else:
|
|
|
235 |
|
236 |
conn.close()
|
237 |
print("Обработка фильмов завершена.")
|
238 |
+
|
239 |
def get_movie_embeddings(conn):
|
240 |
"""Загружает все эмбеддинги фильмов из базы данных."""
|
241 |
movie_embeddings = {}
|
|
|
255 |
def search_movies(query, top_k=10):
|
256 |
"""
|
257 |
Ищет наиболее похожие фильмы по запросу.
|
|
|
258 |
Args:
|
259 |
query: Текстовый запрос.
|
260 |
top_k: Количество возвращаемых результатов.
|
|
|
261 |
Returns:
|
262 |
Строку с результатами поиска в формате HTML.
|
263 |
"""
|
264 |
global search_in_progress
|
265 |
search_in_progress = True
|
266 |
start_time = time.time()
|
267 |
+
|
268 |
print(f"\n\033[1mПоиск по запросу: '{query}'\033[0m")
|
269 |
+
|
270 |
conn = get_db_connection()
|
271 |
if conn is None:
|
272 |
search_in_progress = False
|
273 |
return "<p>Ошибка подключения к базе данных.</p>"
|
274 |
|
275 |
query_crc32 = calculate_crc32(query)
|
276 |
+
|
277 |
# Проверяем, есть ли уже эмбеддинг для этого запроса в кэше
|
278 |
print(f"Начало поиска эмбеддинга запроса в кэше: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
279 |
query_embedding_tensor = get_embedding_from_db(conn, query_cache_table, "query_crc32", query_crc32, model_name)
|
280 |
print(f"Окончание поиска эмбеддинга запроса в кэше: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
281 |
|
282 |
if query_embedding_tensor is None:
|
283 |
+
# Если эмбеддинга нет в кэше, создаем новый
|
284 |
+
print(f"Начало создания эмбеддинга запроса: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
285 |
query_embedding_tensor = encode_string(query)
|
286 |
+
print(f"Окончание создания эмбеддинга запроса: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
287 |
+
|
288 |
+
# Сохраняем эмбеддинг запроса в кэш
|
289 |
insert_embedding(conn, query_cache_table, "query_crc32", query_crc32, {"query": query}, query_embedding_tensor)
|
290 |
+
else:
|
291 |
+
print("Эмбеддинг запроса найден в кэше.")
|
292 |
+
|
293 |
+
# Загружаем эмбеддинги фильмов
|
294 |
+
print(f"Начало загрузки эмбеддингов фильмов: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
295 |
+
movie_embeddings = get_movie_embeddings(conn)
|
296 |
+
print(f"Окончание загрузки эмбеддингов фильмов: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
297 |
+
|
298 |
+
# Вычисляем косинусное сходство
|
299 |
+
print(f"Начало вычисления косинусного сходства: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
300 |
+
similarities = []
|
301 |
+
for title, movie_embedding in movie_embeddings.items():
|
302 |
+
similarity = util.pytorch_cos_sim(query_embedding_tensor, movie_embedding).item()
|
303 |
+
similarities.append((title, similarity))
|
304 |
+
|
305 |
+
# Сортируем результаты
|
306 |
+
similarities.sort(key=lambda x: x[1], reverse=True)
|
307 |
+
top_results = similarities[:top_k]
|
308 |
+
print(f"Окончание вычисления косинусного сходства: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
309 |
|
310 |
+
# Формируем HTML-строку с результатами
|
311 |
+
results_html = "<ol>"
|
312 |
+
for title, score in top_results:
|
313 |
+
results_html += f"<li><strong>{title}</strong> (Сходство: {score:.4f})</li>"
|
314 |
+
results_html += "</ol>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
315 |
|
|
|
|
|
|
|
316 |
search_in_progress = False
|
317 |
+
end_time = time.time()
|
318 |
+
search_time = end_time - start_time
|
319 |
+
print(f"\033[1mПоиск завершен за {search_time:.2f} секунд.\033[0m")
|
320 |
|
321 |
+
return f"<p>Время поиска: {search_time:.2f} секунд</p>" + results_html
|
322 |
+
|
323 |
+
# Запускаем обработку фильмов в отдельном потоке
|
324 |
processing_thread = threading.Thread(target=process_movies)
|
325 |
+
processing_thread.start()
|
326 |
|
327 |
# Создаем интерфейс Gradio
|
328 |
iface = gr.Interface(
|
329 |
fn=search_movies,
|
330 |
+
inputs=gr.Textbox(lines=2, placeholder="Введите запрос для поиска фильмов..."),
|
331 |
+
outputs=gr.HTML(label="Результаты поиска"),
|
332 |
+
title="Семантический поиск фильмов",
|
333 |
+
description="Введите описание фильма, который вы ищете, и система найдет наиболее похожие фильмы."
|
|
|
|
|
|
|
|
|
|
|
334 |
)
|
335 |
|
336 |
+
# Запускаем интерфейс
|
337 |
+
iface.launch()
|
|
|
|
|
|
|
|