Update app.py
Browse files
app.py
CHANGED
@@ -7,6 +7,7 @@ import queue
|
|
7 |
import torch
|
8 |
import psycopg2
|
9 |
import zlib
|
|
|
10 |
from urllib.parse import urlparse
|
11 |
|
12 |
# Настройки базы данных PostgreSQL
|
@@ -46,8 +47,6 @@ except FileNotFoundError:
|
|
46 |
|
47 |
# Очередь для необработанных фильмов
|
48 |
movies_queue = queue.Queue()
|
49 |
-
for movie in movies_data:
|
50 |
-
movies_queue.put(movie)
|
51 |
|
52 |
# Флаг, указывающий, что обработка фильмов завершена
|
53 |
processing_complete = False
|
@@ -71,7 +70,7 @@ def get_db_connection():
|
|
71 |
return None
|
72 |
|
73 |
def setup_database():
|
74 |
-
"""Настраивает базу данных: создает расширение, таблицы и
|
75 |
conn = get_db_connection()
|
76 |
if conn is None:
|
77 |
return
|
@@ -83,57 +82,32 @@ def setup_database():
|
|
83 |
# Создаем таблицу для хранения эмбеддингов фильмов
|
84 |
cur.execute(f"""
|
85 |
CREATE TABLE IF NOT EXISTS {embeddings_table} (
|
86 |
-
movie_id INTEGER,
|
87 |
-
embedding_crc32 BIGINT
|
88 |
string_crc32 BIGINT,
|
89 |
model_name TEXT,
|
90 |
-
embedding
|
91 |
);
|
|
|
92 |
""")
|
93 |
|
94 |
-
# Создаем таблицу для кэширования
|
95 |
cur.execute(f"""
|
96 |
CREATE TABLE IF NOT EXISTS {query_cache_table} (
|
97 |
query_crc32 BIGINT PRIMARY KEY,
|
98 |
query TEXT,
|
99 |
model_name TEXT,
|
100 |
-
embedding
|
101 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
102 |
);
|
103 |
-
CREATE INDEX IF NOT EXISTS
|
104 |
-
CREATE INDEX IF NOT EXISTS
|
105 |
-
""")
|
106 |
-
|
107 |
-
# Создаем функцию и триггер для автоматического удаления старых записей из таблицы кэша запросов
|
108 |
-
cur.execute(f"""
|
109 |
-
CREATE OR REPLACE FUNCTION manage_query_cache_size()
|
110 |
-
RETURNS TRIGGER AS $$
|
111 |
-
DECLARE
|
112 |
-
table_size BIGINT;
|
113 |
-
row_to_delete RECORD;
|
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};
|
121 |
-
END LOOP;
|
122 |
-
END IF;
|
123 |
-
RETURN NEW;
|
124 |
-
END;
|
125 |
-
$$ LANGUAGE plpgsql;
|
126 |
-
|
127 |
-
CREATE OR REPLACE TRIGGER trg_manage_query_cache_size
|
128 |
-
AFTER INSERT ON {query_cache_table}
|
129 |
-
FOR EACH ROW
|
130 |
-
EXECUTE PROCEDURE manage_query_cache_size();
|
131 |
""")
|
132 |
|
133 |
conn.commit()
|
134 |
conn.close()
|
135 |
|
136 |
-
# Настраиваем базу данных при запуске
|
137 |
setup_database()
|
138 |
|
139 |
def calculate_crc32(text):
|
@@ -144,44 +118,78 @@ def encode_string(text):
|
|
144 |
"""Кодирует строку в эмбеддинг."""
|
145 |
return model.encode(text, convert_to_tensor=True, normalize_embeddings=True)
|
146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
def get_embedding_from_db(conn, table_name, crc32_column, crc32_value, model_name):
|
148 |
-
"""
|
149 |
-
Пытается получить эмбеддинг из указанной таблицы по CRC32.
|
150 |
-
Возвращает эмбеддинг, если найден, иначе None.
|
151 |
-
"""
|
152 |
with conn.cursor() as cur:
|
153 |
-
cur.execute(f"SELECT embedding FROM {table_name} WHERE {crc32_column} = %s AND model_name = %s",
|
|
|
154 |
result = cur.fetchone()
|
155 |
-
if result:
|
156 |
-
return
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
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}
|
170 |
-
|
171 |
-
|
172 |
-
|
|
|
173 |
conn.commit()
|
174 |
return True
|
175 |
except Exception as e:
|
176 |
-
print(f"Ошибка при вставке
|
177 |
conn.rollback()
|
178 |
return False
|
179 |
|
180 |
def process_movies():
|
181 |
-
"""
|
182 |
-
Обрабатывает фильмы из очереди, создавая для них эмбеддинги и сохраняя их в базу данных.
|
183 |
-
"""
|
184 |
global processing_complete
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
conn = get_db_connection()
|
186 |
if conn is None:
|
187 |
processing_complete = True
|
@@ -189,136 +197,108 @@ def process_movies():
|
|
189 |
|
190 |
while True:
|
191 |
if search_in_progress:
|
192 |
-
time.sleep(1)
|
193 |
continue
|
194 |
|
195 |
batch = []
|
196 |
while not movies_queue.empty() and len(batch) < batch_size:
|
197 |
try:
|
198 |
-
movie = movies_queue.
|
199 |
batch.append(movie)
|
200 |
except queue.Empty:
|
201 |
break
|
202 |
|
203 |
if not batch:
|
204 |
-
print("Очередь фильмов пуста.")
|
205 |
-
processing_complete = True
|
206 |
break
|
207 |
|
208 |
-
|
209 |
-
embedding_strings = [
|
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 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
220 |
|
221 |
-
|
222 |
-
|
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:
|
232 |
-
print(f"Ошибка сохранения эмбеддинга для фильма '{movie['name']}'.")
|
233 |
else:
|
234 |
-
print(f"
|
|
|
|
|
235 |
|
236 |
conn.close()
|
237 |
-
|
|
|
238 |
|
239 |
def get_movie_embeddings(conn):
|
240 |
"""Загружает все эмбеддинги фильмов из базы данных."""
|
241 |
movie_embeddings = {}
|
242 |
with conn.cursor() as cur:
|
243 |
-
cur.execute(f"
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
|
|
248 |
for movie in movies_data:
|
249 |
if movie['id'] == movie_id:
|
250 |
-
|
251 |
-
movie_embeddings[title] = torch.tensor(embedding)
|
252 |
break
|
253 |
return movie_embeddings
|
254 |
|
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 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
287 |
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
|
|
304 |
|
305 |
-
|
306 |
-
|
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)
|
|
|
7 |
import torch
|
8 |
import psycopg2
|
9 |
import zlib
|
10 |
+
import numpy as np
|
11 |
from urllib.parse import urlparse
|
12 |
|
13 |
# Настройки базы данных PostgreSQL
|
|
|
47 |
|
48 |
# Очередь для необработанных фильмов
|
49 |
movies_queue = queue.Queue()
|
|
|
|
|
50 |
|
51 |
# Флаг, указывающий, что обработка фильмов завершена
|
52 |
processing_complete = False
|
|
|
70 |
return None
|
71 |
|
72 |
def setup_database():
|
73 |
+
"""Настраивает базу данных: создает расширение, таблицы и индексы."""
|
74 |
conn = get_db_connection()
|
75 |
if conn is None:
|
76 |
return
|
|
|
82 |
# Создаем таблицу для хранения эмбеддингов фильмов
|
83 |
cur.execute(f"""
|
84 |
CREATE TABLE IF NOT EXISTS {embeddings_table} (
|
85 |
+
movie_id INTEGER PRIMARY KEY,
|
86 |
+
embedding_crc32 BIGINT,
|
87 |
string_crc32 BIGINT,
|
88 |
model_name TEXT,
|
89 |
+
embedding float8[]
|
90 |
);
|
91 |
+
CREATE INDEX IF NOT EXISTS idx_movie_embeddings_crc32 ON {embeddings_table} (string_crc32);
|
92 |
""")
|
93 |
|
94 |
+
# Создаем таблицу для кэширования запросов
|
95 |
cur.execute(f"""
|
96 |
CREATE TABLE IF NOT EXISTS {query_cache_table} (
|
97 |
query_crc32 BIGINT PRIMARY KEY,
|
98 |
query TEXT,
|
99 |
model_name TEXT,
|
100 |
+
embedding float8[],
|
101 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
102 |
);
|
103 |
+
CREATE INDEX IF NOT EXISTS idx_query_cache_crc32 ON {query_cache_table} (query_crc32);
|
104 |
+
CREATE INDEX IF NOT EXISTS idx_query_cache_created ON {query_cache_table} (created_at);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
""")
|
106 |
|
107 |
conn.commit()
|
108 |
conn.close()
|
109 |
|
110 |
+
# Настраиваем базу данных при запуске
|
111 |
setup_database()
|
112 |
|
113 |
def calculate_crc32(text):
|
|
|
118 |
"""Кодирует строку в эмбеддинг."""
|
119 |
return model.encode(text, convert_to_tensor=True, normalize_embeddings=True)
|
120 |
|
121 |
+
def get_movies_without_embeddings():
|
122 |
+
"""Получает список фильмов, для которых нужно создать эмбеддинги."""
|
123 |
+
conn = get_db_connection()
|
124 |
+
if conn is None:
|
125 |
+
return []
|
126 |
+
|
127 |
+
movies_to_process = []
|
128 |
+
with conn.cursor() as cur:
|
129 |
+
# Получаем список ID фильмов, которые уже есть в базе
|
130 |
+
cur.execute(f"SELECT movie_id FROM {embeddings_table}")
|
131 |
+
existing_ids = {row[0] for row in cur.fetchall()}
|
132 |
+
|
133 |
+
# Фильтруем только те фильмы, которых нет в базе
|
134 |
+
for movie in movies_data:
|
135 |
+
if movie['id'] not in existing_ids:
|
136 |
+
movies_to_process.append(movie)
|
137 |
+
|
138 |
+
conn.close()
|
139 |
+
return movies_to_process
|
140 |
+
|
141 |
+
def vector_to_list(vector):
|
142 |
+
"""Преобразует вектор PyTorch в список float."""
|
143 |
+
return vector.detach().cpu().numpy().tolist()
|
144 |
+
|
145 |
+
def list_to_vector(lst):
|
146 |
+
"""Преобразует список float в вектор PyTorch."""
|
147 |
+
return torch.tensor(lst)
|
148 |
+
|
149 |
def get_embedding_from_db(conn, table_name, crc32_column, crc32_value, model_name):
|
150 |
+
"""Получает эмбеддинг из базы данных."""
|
|
|
|
|
|
|
151 |
with conn.cursor() as cur:
|
152 |
+
cur.execute(f"SELECT embedding FROM {table_name} WHERE {crc32_column} = %s AND model_name = %s",
|
153 |
+
(crc32_value, model_name))
|
154 |
result = cur.fetchone()
|
155 |
+
if result and result[0]:
|
156 |
+
return list_to_vector(result[0])
|
157 |
+
return None
|
158 |
+
|
159 |
+
def insert_embedding(conn, table_name, movie_id, embedding_crc32, string_crc32, embedding):
|
160 |
+
"""Вставляет эмбеддинг в базу данных."""
|
161 |
+
embedding_list = vector_to_list(embedding)
|
|
|
|
|
|
|
|
|
162 |
with conn.cursor() as cur:
|
163 |
try:
|
164 |
cur.execute(f"""
|
165 |
+
INSERT INTO {table_name}
|
166 |
+
(movie_id, embedding_crc32, string_crc32, model_name, embedding)
|
167 |
+
VALUES (%s, %s, %s, %s, %s)
|
168 |
+
ON CONFLICT (movie_id) DO NOTHING
|
169 |
+
""", (movie_id, embedding_crc32, string_crc32, model_name, embedding_list))
|
170 |
conn.commit()
|
171 |
return True
|
172 |
except Exception as e:
|
173 |
+
print(f"Ошибка при вставке эмбеддинга: {e}")
|
174 |
conn.rollback()
|
175 |
return False
|
176 |
|
177 |
def process_movies():
|
178 |
+
"""Обрабатывает фильмы, создавая для них эмбеддинги."""
|
|
|
|
|
179 |
global processing_complete
|
180 |
+
|
181 |
+
# Получаем список фильмов, которые нужно обработать
|
182 |
+
movies_to_process = get_movies_without_embeddings()
|
183 |
+
|
184 |
+
if not movies_to_process:
|
185 |
+
print("Все фильмы уже обработаны.")
|
186 |
+
processing_complete = True
|
187 |
+
return
|
188 |
+
|
189 |
+
# Добавляем фильмы в очередь
|
190 |
+
for movie in movies_to_process:
|
191 |
+
movies_queue.put(movie)
|
192 |
+
|
193 |
conn = get_db_connection()
|
194 |
if conn is None:
|
195 |
processing_complete = True
|
|
|
197 |
|
198 |
while True:
|
199 |
if search_in_progress:
|
200 |
+
time.sleep(1)
|
201 |
continue
|
202 |
|
203 |
batch = []
|
204 |
while not movies_queue.empty() and len(batch) < batch_size:
|
205 |
try:
|
206 |
+
movie = movies_queue.get_nowait()
|
207 |
batch.append(movie)
|
208 |
except queue.Empty:
|
209 |
break
|
210 |
|
211 |
if not batch:
|
|
|
|
|
212 |
break
|
213 |
|
214 |
+
print(f"Обработка пакета из {len(batch)} фильмов...")
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
|
216 |
+
for movie in batch:
|
217 |
+
embedding_string = f"Название: {movie['name']}\nГод: {movie['year']}\nЖанры: {movie['genresList']}\nОписание: {movie['description']}"
|
218 |
+
string_crc32 = calculate_crc32(embedding_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(embedding_string)
|
225 |
+
embedding_crc32 = calculate_crc32(str(vector_to_list(embedding)))
|
226 |
|
227 |
+
if insert_embedding(conn, embeddings_table, movie['id'], embedding_crc32, string_crc32, embedding):
|
228 |
+
print(f"Сохранен эмбеддинг для '{movie['name']}'")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
else:
|
230 |
+
print(f"Ошибка сохранения эмбеддинга для '{movie['name']}'")
|
231 |
+
else:
|
232 |
+
print(f"Эмбеддинг для '{movie['name']}' уже существует")
|
233 |
|
234 |
conn.close()
|
235 |
+
processing_complete = True
|
236 |
+
print("Обработка фильмов завершена")
|
237 |
|
238 |
def get_movie_embeddings(conn):
|
239 |
"""Загружает все эмбеддинги фильмов из базы данных."""
|
240 |
movie_embeddings = {}
|
241 |
with conn.cursor() as cur:
|
242 |
+
cur.execute(f"""
|
243 |
+
SELECT e.movie_id, e.embedding
|
244 |
+
FROM {embeddings_table} e
|
245 |
+
""")
|
246 |
+
for movie_id, embedding in cur.fetchall():
|
247 |
+
# Находим название фильма по ID
|
248 |
for movie in movies_data:
|
249 |
if movie['id'] == movie_id:
|
250 |
+
movie_embeddings[movie['name']] = list_to_vector(embedding)
|
|
|
251 |
break
|
252 |
return movie_embeddings
|
253 |
|
254 |
def search_movies(query, top_k=10):
|
255 |
+
"""Выполняет поиск фильмов по запросу."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
256 |
global search_in_progress
|
257 |
search_in_progress = True
|
258 |
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
259 |
|
260 |
+
try:
|
261 |
+
conn = get_db_connection()
|
262 |
+
if conn is None:
|
263 |
+
return "<p>Ошибка подключения к базе данных</p>"
|
264 |
+
|
265 |
+
query_crc32 = calculate_crc32(query)
|
266 |
+
query_embedding = get_embedding_from_db(conn, query_cache_table, "query_crc32", query_crc32, model_name)
|
267 |
+
|
268 |
+
if query_embedding is None:
|
269 |
+
query_embedding = encode_string(query)
|
270 |
+
embedding_list = vector_to_list(query_embedding)
|
271 |
+
|
272 |
+
with conn.cursor() as cur:
|
273 |
+
cur.execute(f"""
|
274 |
+
INSERT INTO {query_cache_table} (query_crc32, query, model_name, embedding)
|
275 |
+
VALUES (%s, %s, %s, %s)
|
276 |
+
ON CONFLICT (query_crc32) DO NOTHING
|
277 |
+
""", (query_crc32, query, model_name, embedding_list))
|
278 |
+
conn.commit()
|
279 |
+
|
280 |
+
movie_embeddings = get_movie_embeddings(conn)
|
281 |
|
282 |
+
similarities = []
|
283 |
+
for title, movie_embedding in movie_embeddings.items():
|
284 |
+
similarity = util.pytorch_cos_sim(query_embedding, movie_embedding).item()
|
285 |
+
similarities.append((title, similarity))
|
286 |
+
|
287 |
+
similarities.sort(key=lambda x: x[1], reverse=True)
|
288 |
+
top_results = similarities[:top_k]
|
289 |
+
|
290 |
+
results_html = "<ol>"
|
291 |
+
for title, score in top_results:
|
292 |
+
results_html += f"<li><strong>{title}</strong> (Сходство: {score:.4f})</li>"
|
293 |
+
results_html += "</ol>"
|
294 |
+
|
295 |
+
search_time = time.time() - start_time
|
296 |
+
conn.close()
|
297 |
+
|
298 |
+
return f"<p>Время поиска: {search_time:.2f} сек</p>{results_html}"
|
299 |
|
300 |
+
finally:
|
301 |
+
search_in_progress = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
302 |
|
303 |
# Запускаем обработку фильмов в отдельном потоке
|
304 |
processing_thread = threading.Thread(target=process_movies)
|