Update app.py
Browse files
    	
        app.py
    CHANGED
    
    | @@ -1,23 +1,17 @@ | |
|  | |
|  | |
| 1 | 
             
            import os
         | 
| 2 | 
             
            import time
         | 
| 3 | 
             
            import threading
         | 
| 4 | 
             
            import queue
         | 
| 5 | 
            -
            from typing import List, Dict, Any, Optional
         | 
| 6 | 
            -
            import logging
         | 
| 7 | 
            -
            from urllib.parse import urlparse
         | 
| 8 | 
            -
             | 
| 9 | 
            -
            import gradio as gr
         | 
| 10 | 
             
            import torch
         | 
| 11 | 
             
            import psycopg2
         | 
| 12 | 
             
            import zlib
         | 
| 13 | 
             
            import numpy as np
         | 
| 14 | 
            -
            from  | 
|  | |
| 15 | 
             
            from sklearn.preprocessing import normalize
         | 
| 16 |  | 
| 17 | 
            -
            # Рекомендуется использовать python-dotenv для загрузки переменных окружения
         | 
| 18 | 
            -
            # from dotenv import load_dotenv
         | 
| 19 | 
            -
            # load_dotenv()
         | 
| 20 | 
            -
             | 
| 21 | 
             
            # Настройка логирования
         | 
| 22 | 
             
            logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
         | 
| 23 |  | 
| @@ -60,10 +54,12 @@ except FileNotFoundError: | |
| 60 | 
             
                movies_data = []
         | 
| 61 |  | 
| 62 | 
             
            # Очередь для необработанных фильмов
         | 
| 63 | 
            -
            movies_queue | 
| 64 |  | 
| 65 | 
            -
            #  | 
| 66 | 
             
            processing_complete = False
         | 
|  | |
|  | |
| 67 | 
             
            search_in_progress = False
         | 
| 68 |  | 
| 69 | 
             
            # Блокировка для доступа к базе данных
         | 
| @@ -83,19 +79,20 @@ def get_db_connection(): | |
| 83 |  | 
| 84 | 
             
            def setup_database():
         | 
| 85 | 
             
                """Настраивает базу данных: создает расширение, таблицы и индексы."""
         | 
| 86 | 
            -
                 | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
|  | |
| 99 | 
             
                            CREATE TABLE {embeddings_table} (
         | 
| 100 | 
             
                                movie_id INTEGER PRIMARY KEY,
         | 
| 101 | 
             
                                embedding_crc32 BIGINT,
         | 
| @@ -104,10 +101,10 @@ def setup_database(): | |
| 104 | 
             
                                embedding vector(1024)
         | 
| 105 | 
             
                            );
         | 
| 106 | 
             
                            CREATE INDEX ON {embeddings_table} (string_crc32);
         | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
             
                            CREATE TABLE {query_cache_table} (
         | 
| 112 | 
             
                                query_crc32 BIGINT PRIMARY KEY,
         | 
| 113 | 
             
                                query TEXT,
         | 
| @@ -117,51 +114,60 @@ def setup_database(): | |
| 117 | 
             
                            );
         | 
| 118 | 
             
                            CREATE INDEX ON {query_cache_table} (query_crc32);
         | 
| 119 | 
             
                            CREATE INDEX ON {query_cache_table} (created_at);
         | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
|  | |
|  | |
| 127 |  | 
| 128 | 
             
            # Настраиваем базу данных при запуске
         | 
| 129 | 
             
            setup_database()
         | 
| 130 |  | 
| 131 | 
            -
            def calculate_crc32(text | 
| 132 | 
             
                """Вычисляет CRC32 для строки."""
         | 
| 133 | 
             
                return zlib.crc32(text.encode('utf-8')) & 0xFFFFFFFF
         | 
| 134 |  | 
| 135 | 
            -
            def encode_string(text | 
| 136 | 
             
                """Кодирует строку в эмбеддинг."""
         | 
| 137 | 
             
                embedding = model.encode(text, convert_to_tensor=True, normalize_embeddings=True)
         | 
| 138 | 
             
                return embedding.cpu().numpy()
         | 
| 139 |  | 
| 140 | 
            -
            def get_movies_without_embeddings() | 
| 141 | 
             
                """Получает список фильм��в, для которых нужно создать эмбеддинги."""
         | 
| 142 | 
            -
                 | 
| 143 | 
            -
             | 
| 144 | 
            -
             | 
| 145 | 
            -
             | 
| 146 | 
            -
             | 
| 147 | 
            -
             | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
             | 
| 151 | 
            -
             | 
| 152 | 
            -
             | 
| 153 | 
            -
             | 
| 154 | 
            -
                         | 
| 155 | 
            -
             | 
| 156 | 
            -
             | 
| 157 | 
            -
             | 
| 158 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
| 159 |  | 
| 160 | 
            -
             | 
|  | |
|  | |
| 161 | 
             
                """Получает эмбеддинг из базы данных."""
         | 
| 162 | 
             
                try:
         | 
| 163 | 
             
                    with conn.cursor() as cur:
         | 
| 164 | 
            -
                        cur.execute(f"SELECT embedding FROM {table_name} WHERE {crc32_column} = %s AND model_name = %s",  | 
|  | |
| 165 | 
             
                        result = cur.fetchone()
         | 
| 166 | 
             
                        if result and result[0]:
         | 
| 167 | 
             
                            # Нормализуем эмбеддинг после извлечения из БД
         | 
| @@ -170,16 +176,17 @@ def get_embedding_from_db(conn, table_name: str, crc32_column: str, crc32_value: | |
| 170 | 
             
                    logging.error(f"Ошибка при получении эмбеддинга из БД: {e}")
         | 
| 171 | 
             
                return None
         | 
| 172 |  | 
| 173 | 
            -
            def insert_embedding(conn, table_name | 
| 174 | 
             
                """Вставляет эмбеддинг в базу данных."""
         | 
| 175 | 
             
                try:
         | 
| 176 | 
             
                    # Нормализуем эмбеддинг перед сохранением
         | 
| 177 | 
             
                    normalized_embedding = normalize(embedding.reshape(1, -1))[0]
         | 
| 178 | 
             
                    with conn.cursor() as cur:
         | 
| 179 | 
             
                        cur.execute(f"""
         | 
| 180 | 
            -
             | 
| 181 | 
            -
             | 
| 182 | 
            -
             | 
|  | |
| 183 | 
             
                        """, (movie_id, embedding_crc32, string_crc32, model_name, normalized_embedding.tolist()))
         | 
| 184 | 
             
                    conn.commit()
         | 
| 185 | 
             
                    return True
         | 
| @@ -191,10 +198,12 @@ def insert_embedding(conn, table_name: str, movie_id: int, embedding_crc32: int, | |
| 191 | 
             
            def process_movies():
         | 
| 192 | 
             
                """Обрабатывает фильмы, создавая для них эмбеддинги."""
         | 
| 193 | 
             
                global processing_complete
         | 
|  | |
| 194 | 
             
                logging.info("Начало обработки фильмов.")
         | 
| 195 | 
            -
             | 
| 196 | 
             
                # Получаем список фильмов, которые нужно обработать
         | 
| 197 | 
             
                movies_to_process = get_movies_without_embeddings()
         | 
|  | |
| 198 | 
             
                if not movies_to_process:
         | 
| 199 | 
             
                    logging.info("Все фильмы уже обработаны.")
         | 
| 200 | 
             
                    processing_complete = True
         | 
| @@ -204,51 +213,55 @@ def process_movies(): | |
| 204 | 
             
                for movie in movies_to_process:
         | 
| 205 | 
             
                    movies_queue.put(movie)
         | 
| 206 |  | 
| 207 | 
            -
                 | 
| 208 | 
            -
             | 
| 209 | 
            -
             | 
| 210 | 
            -
             | 
| 211 | 
            -
                    try:
         | 
| 212 | 
            -
                        while not movies_queue.empty():
         | 
| 213 | 
            -
                            if search_in_progress:
         | 
| 214 | 
            -
                                time.sleep(1)
         | 
| 215 | 
            -
                                continue
         | 
| 216 | 
            -
             | 
| 217 | 
            -
                            batch = []
         | 
| 218 | 
            -
                            while not movies_queue.empty() and len(batch) < batch_size:
         | 
| 219 | 
            -
                                try:
         | 
| 220 | 
            -
                                    movie = movies_queue.get_nowait()
         | 
| 221 | 
            -
                                    batch.append(movie)
         | 
| 222 | 
            -
                                except queue.Empty:
         | 
| 223 | 
            -
                                    break
         | 
| 224 |  | 
| 225 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 226 | 
             
                                break
         | 
| 227 |  | 
| 228 | 
            -
             | 
| 229 | 
            -
                             | 
| 230 | 
            -
                                embedding_string = f"Название: {movie['name']}\nГод: {movie['year']}\nЖанры: {movie['genresList']}\nОписание: {movie['description']}"
         | 
| 231 | 
            -
                                string_crc32 = calculate_crc32(embedding_string)
         | 
| 232 | 
            -
             | 
| 233 | 
            -
                                # Проверяем существующий эмбеддинг
         | 
| 234 | 
            -
                                existing_embedding = get_embedding_from_db(conn, embeddings_table, "string_crc32", string_crc32, model_name)
         | 
| 235 | 
            -
                                if existing_embedding is None:
         | 
| 236 | 
            -
                                    embedding = encode_string(embedding_string)
         | 
| 237 | 
            -
                                    embedding_crc32 = calculate_crc32(str(embedding.tolist()))
         | 
| 238 | 
            -
                                    if insert_embedding(conn, embeddings_table, movie['id'], embedding_crc32, string_crc32, embedding):
         | 
| 239 | 
            -
                                        logging.info(f"Сохранен эмбеддинг для '{movie['name']}'")
         | 
| 240 | 
            -
                                    else:
         | 
| 241 | 
            -
                                        logging.error(f"Ошибка сохранения эмбеддинга для '{movie['name']}'")
         | 
| 242 | 
            -
                                else:
         | 
| 243 | 
            -
                                    logging.info(f"Эмбеддинг для '{movie['name']}' уже существует")
         | 
| 244 |  | 
| 245 | 
            -
             | 
| 246 | 
            -
             | 
| 247 | 
            -
             | 
| 248 | 
            -
             | 
| 249 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 250 |  | 
| 251 | 
            -
            def get_movie_embeddings(conn) | 
| 252 | 
             
                """Загружает все эмбеддинги фильмов из базы данных."""
         | 
| 253 | 
             
                movie_embeddings = {}
         | 
| 254 | 
             
                try:
         | 
| @@ -265,97 +278,96 @@ def get_movie_embeddings(conn) -> Dict[str, np.ndarray]: | |
| 265 | 
             
                    logging.error(f"Ошибка при загрузке эмбеддингов фильмов: {e}")
         | 
| 266 | 
             
                return movie_embeddings
         | 
| 267 |  | 
| 268 | 
            -
            def  | 
| 269 | 
            -
                """Очищает устаревшие записи из кэша запросов."""
         | 
| 270 | 
            -
                try:
         | 
| 271 | 
            -
                    with conn.cursor() as cur:
         | 
| 272 | 
            -
                        # Получаем общий размер кэша
         | 
| 273 | 
            -
                        cur.execute(f"SELECT pg_total_relation_size('{query_cache_table}')")
         | 
| 274 | 
            -
                        total_size = cur.fetchone()[0]
         | 
| 275 | 
            -
                        
         | 
| 276 | 
            -
                        if total_size > MAX_CACHE_SIZE:
         | 
| 277 | 
            -
                            # Удаляем старые записи, пока размер не стан��т меньше максимального
         | 
| 278 | 
            -
                            cur.execute(f"""
         | 
| 279 | 
            -
                            DELETE FROM {query_cache_table}
         | 
| 280 | 
            -
                            WHERE ctid IN (
         | 
| 281 | 
            -
                                SELECT ctid
         | 
| 282 | 
            -
                                FROM {query_cache_table}
         | 
| 283 | 
            -
                                ORDER BY created_at ASC
         | 
| 284 | 
            -
                                LIMIT (SELECT COUNT(*) / 2 FROM {query_cache_table})
         | 
| 285 | 
            -
                            )
         | 
| 286 | 
            -
                            """)
         | 
| 287 | 
            -
                            conn.commit()
         | 
| 288 | 
            -
                            logging.info("Кэш запросов очищен.")
         | 
| 289 | 
            -
                except Exception as e:
         | 
| 290 | 
            -
                    logging.error(f"Ошибка при очистке кэша запросов: {e}")
         | 
| 291 | 
            -
                    conn.rollback()
         | 
| 292 | 
            -
             | 
| 293 | 
            -
            def search_movies(query: str, top_k: int = 10) -> List[Dict[str, Any]]:
         | 
| 294 | 
             
                """Выполняет поиск фильмов по запросу."""
         | 
| 295 | 
             
                global search_in_progress
         | 
| 296 | 
             
                search_in_progress = True
         | 
| 297 | 
            -
                
         | 
|  | |
| 298 | 
             
                try:
         | 
| 299 | 
            -
                     | 
| 300 | 
            -
             | 
| 301 | 
            -
             | 
| 302 |  | 
| 303 | 
            -
             | 
|  | |
| 304 |  | 
| 305 | 
            -
             | 
| 306 | 
            -
                        query_embedding =  | 
| 307 |  | 
| 308 | 
            -
                         | 
| 309 | 
            -
                             | 
| 310 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 311 |  | 
| 312 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 313 |  | 
| 314 | 
            -
             | 
| 315 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 316 |  | 
| 317 | 
            -
                         | 
| 318 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 319 |  | 
| 320 | 
            -
                        results = []
         | 
| 321 | 
            -
                        for score, movie_name in top_results:
         | 
| 322 | 
            -
                            movie = next((m for m in movies_data if m['name'] == movie_name), None)
         | 
| 323 | 
            -
                            if movie:
         | 
| 324 | 
            -
                                results.append({
         | 
| 325 | 
            -
                                    "name": movie['name'],
         | 
| 326 | 
            -
                                    "year": movie['year'],
         | 
| 327 | 
            -
                                    "genres": movie['genresList'],
         | 
| 328 | 
            -
                                    "description": movie['description'],
         | 
| 329 | 
            -
                                    "score": float(score)
         | 
| 330 | 
            -
                                })
         | 
| 331 | 
            -
             | 
| 332 | 
            -
                    return results
         | 
| 333 | 
             
                except Exception as e:
         | 
| 334 | 
            -
                    logging.error(f"Ошибка при  | 
| 335 | 
            -
                    return  | 
|  | |
| 336 | 
             
                finally:
         | 
|  | |
|  | |
| 337 | 
             
                    search_in_progress = False
         | 
| 338 |  | 
| 339 | 
             
            # Запускаем обработку фильмов в отдельном потоке
         | 
| 340 | 
            -
            threading.Thread(target=process_movies | 
|  | |
| 341 |  | 
| 342 | 
             
            # Создаем интерфейс Gradio
         | 
| 343 | 
            -
            def gradio_search(query: str) -> str:
         | 
| 344 | 
            -
                results = search_movies(query)
         | 
| 345 | 
            -
                output = ""
         | 
| 346 | 
            -
                for movie in results:
         | 
| 347 | 
            -
                    output += f"Название: {movie['name']} ({movie['year']})\n"
         | 
| 348 | 
            -
                    output += f"Жанры: {', '.join(movie['genres'])}\n"
         | 
| 349 | 
            -
                    output += f"Описание: {movie['description']}\n"
         | 
| 350 | 
            -
                    output += f"Релевантность: {movie['score']:.2f}\n\n"
         | 
| 351 | 
            -
                return output
         | 
| 352 | 
            -
             | 
| 353 | 
             
            iface = gr.Interface(
         | 
| 354 | 
            -
                fn= | 
| 355 | 
            -
                inputs=" | 
| 356 | 
            -
                outputs=" | 
| 357 | 
            -
                title=" | 
| 358 | 
            -
                description="Введите  | 
| 359 | 
             
            )
         | 
| 360 |  | 
|  | |
| 361 | 
             
            iface.launch()
         | 
|  | |
| 1 | 
            +
            import gradio as gr
         | 
| 2 | 
            +
            from sentence_transformers import SentenceTransformer, util
         | 
| 3 | 
             
            import os
         | 
| 4 | 
             
            import time
         | 
| 5 | 
             
            import threading
         | 
| 6 | 
             
            import queue
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 7 | 
             
            import torch
         | 
| 8 | 
             
            import psycopg2
         | 
| 9 | 
             
            import zlib
         | 
| 10 | 
             
            import numpy as np
         | 
| 11 | 
            +
            from urllib.parse import urlparse
         | 
| 12 | 
            +
            import logging
         | 
| 13 | 
             
            from sklearn.preprocessing import normalize
         | 
| 14 |  | 
|  | |
|  | |
|  | |
|  | |
| 15 | 
             
            # Настройка логирования
         | 
| 16 | 
             
            logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
         | 
| 17 |  | 
|  | |
| 54 | 
             
                movies_data = []
         | 
| 55 |  | 
| 56 | 
             
            # Очередь для необработанных фильмов
         | 
| 57 | 
            +
            movies_queue = queue.Queue()
         | 
| 58 |  | 
| 59 | 
            +
            # Флаг, указывающий, что обработка фильмов завершена
         | 
| 60 | 
             
            processing_complete = False
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            # Флаг, указывающий, что выполняется поиск
         | 
| 63 | 
             
            search_in_progress = False
         | 
| 64 |  | 
| 65 | 
             
            # Блокировка для доступа к базе данных
         | 
|  | |
| 79 |  | 
| 80 | 
             
            def setup_database():
         | 
| 81 | 
             
                """Настраивает базу данных: создает расширение, таблицы и индексы."""
         | 
| 82 | 
            +
                conn = get_db_connection()
         | 
| 83 | 
            +
                if conn is None:
         | 
| 84 | 
            +
                    return
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                try:
         | 
| 87 | 
            +
                    with conn.cursor() as cur:
         | 
| 88 | 
            +
                        # Создаем расширение pgvector если его нет
         | 
| 89 | 
            +
                        cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                        # Удаляем существующие таблицы если они есть
         | 
| 92 | 
            +
                        # cur.execute(f"DROP TABLE IF EXISTS {embeddings_table}, {query_cache_table};")
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                        # Создаем таблицу для хранения эмбеддингов фильмов
         | 
| 95 | 
            +
                        cur.execute(f"""
         | 
| 96 | 
             
                            CREATE TABLE {embeddings_table} (
         | 
| 97 | 
             
                                movie_id INTEGER PRIMARY KEY,
         | 
| 98 | 
             
                                embedding_crc32 BIGINT,
         | 
|  | |
| 101 | 
             
                                embedding vector(1024)
         | 
| 102 | 
             
                            );
         | 
| 103 | 
             
                            CREATE INDEX ON {embeddings_table} (string_crc32);
         | 
| 104 | 
            +
                        """)
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                        # Создаем таблицу для кэширования запросов
         | 
| 107 | 
            +
                        cur.execute(f"""
         | 
| 108 | 
             
                            CREATE TABLE {query_cache_table} (
         | 
| 109 | 
             
                                query_crc32 BIGINT PRIMARY KEY,
         | 
| 110 | 
             
                                query TEXT,
         | 
|  | |
| 114 | 
             
                            );
         | 
| 115 | 
             
                            CREATE INDEX ON {query_cache_table} (query_crc32);
         | 
| 116 | 
             
                            CREATE INDEX ON {query_cache_table} (created_at);
         | 
| 117 | 
            +
                        """)
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    conn.commit()
         | 
| 120 | 
            +
                    logging.info("База данных успешно настроена.")
         | 
| 121 | 
            +
                except Exception as e:
         | 
| 122 | 
            +
                    logging.error(f"Ошибка при настройке базы данных: {e}")
         | 
| 123 | 
            +
                    conn.rollback()
         | 
| 124 | 
            +
                finally:
         | 
| 125 | 
            +
                    conn.close()
         | 
| 126 |  | 
| 127 | 
             
            # Настраиваем базу данных при запуске
         | 
| 128 | 
             
            setup_database()
         | 
| 129 |  | 
| 130 | 
            +
            def calculate_crc32(text):
         | 
| 131 | 
             
                """Вычисляет CRC32 для строки."""
         | 
| 132 | 
             
                return zlib.crc32(text.encode('utf-8')) & 0xFFFFFFFF
         | 
| 133 |  | 
| 134 | 
            +
            def encode_string(text):
         | 
| 135 | 
             
                """Кодирует строку в эмбеддинг."""
         | 
| 136 | 
             
                embedding = model.encode(text, convert_to_tensor=True, normalize_embeddings=True)
         | 
| 137 | 
             
                return embedding.cpu().numpy()
         | 
| 138 |  | 
| 139 | 
            +
            def get_movies_without_embeddings():
         | 
| 140 | 
             
                """Получает список фильм��в, для которых нужно создать эмбеддинги."""
         | 
| 141 | 
            +
                conn = get_db_connection()
         | 
| 142 | 
            +
                if conn is None:
         | 
| 143 | 
            +
                    return []
         | 
| 144 | 
            +
             | 
| 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 | 
            +
                        # Фильтруем только те фильмы, которых нет в базе
         | 
| 153 | 
            +
                        for movie in movies_data:
         | 
| 154 | 
            +
                            if movie['id'] not in existing_ids:
         | 
| 155 | 
            +
                                movies_to_process.append(movie)
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                    logging.info(f"Найдено {len(movies_to_process)} фильмов для обработки.")
         | 
| 158 | 
            +
                except Exception as e:
         | 
| 159 | 
            +
                    logging.error(f"Ошибка при получении списка фильмов для обработки: {e}")
         | 
| 160 | 
            +
                finally:
         | 
| 161 | 
            +
                    conn.close()
         | 
| 162 |  | 
| 163 | 
            +
                return movies_to_process
         | 
| 164 | 
            +
             | 
| 165 | 
            +
            def get_embedding_from_db(conn, table_name, crc32_column, crc32_value, model_name):
         | 
| 166 | 
             
                """Получает эмбеддинг из базы данных."""
         | 
| 167 | 
             
                try:
         | 
| 168 | 
             
                    with conn.cursor() as cur:
         | 
| 169 | 
            +
                        cur.execute(f"SELECT embedding FROM {table_name} WHERE {crc32_column} = %s AND model_name = %s", 
         | 
| 170 | 
            +
                                   (crc32_value, model_name))
         | 
| 171 | 
             
                        result = cur.fetchone()
         | 
| 172 | 
             
                        if result and result[0]:
         | 
| 173 | 
             
                            # Нормализуем эмбеддинг после извлечения из БД
         | 
|  | |
| 176 | 
             
                    logging.error(f"Ошибка при получении эмбеддинга из БД: {e}")
         | 
| 177 | 
             
                return None
         | 
| 178 |  | 
| 179 | 
            +
            def insert_embedding(conn, table_name, movie_id, embedding_crc32, string_crc32, embedding):
         | 
| 180 | 
             
                """Вставляет эмбеддинг в базу данных."""
         | 
| 181 | 
             
                try:
         | 
| 182 | 
             
                    # Нормализуем эмбеддинг перед сохранением
         | 
| 183 | 
             
                    normalized_embedding = normalize(embedding.reshape(1, -1))[0]
         | 
| 184 | 
             
                    with conn.cursor() as cur:
         | 
| 185 | 
             
                        cur.execute(f"""
         | 
| 186 | 
            +
                            INSERT INTO {table_name} 
         | 
| 187 | 
            +
                            (movie_id, embedding_crc32, string_crc32, model_name, embedding)
         | 
| 188 | 
            +
                            VALUES (%s, %s, %s, %s, %s)
         | 
| 189 | 
            +
                            ON CONFLICT (movie_id) DO NOTHING
         | 
| 190 | 
             
                        """, (movie_id, embedding_crc32, string_crc32, model_name, normalized_embedding.tolist()))
         | 
| 191 | 
             
                    conn.commit()
         | 
| 192 | 
             
                    return True
         | 
|  | |
| 198 | 
             
            def process_movies():
         | 
| 199 | 
             
                """Обрабатывает фильмы, создавая для них эмбеддинги."""
         | 
| 200 | 
             
                global processing_complete
         | 
| 201 | 
            +
             | 
| 202 | 
             
                logging.info("Начало обработки фильмов.")
         | 
| 203 | 
            +
             | 
| 204 | 
             
                # Получаем список фильмов, которые нужно обработать
         | 
| 205 | 
             
                movies_to_process = get_movies_without_embeddings()
         | 
| 206 | 
            +
             | 
| 207 | 
             
                if not movies_to_process:
         | 
| 208 | 
             
                    logging.info("Все фильмы уже обработаны.")
         | 
| 209 | 
             
                    processing_complete = True
         | 
|  | |
| 213 | 
             
                for movie in movies_to_process:
         | 
| 214 | 
             
                    movies_queue.put(movie)
         | 
| 215 |  | 
| 216 | 
            +
                conn = get_db_connection()
         | 
| 217 | 
            +
                if conn is None:
         | 
| 218 | 
            +
                    processing_complete = True
         | 
| 219 | 
            +
                    return
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 220 |  | 
| 221 | 
            +
                try:
         | 
| 222 | 
            +
                    while not movies_queue.empty():
         | 
| 223 | 
            +
                        if search_in_progress:
         | 
| 224 | 
            +
                            time.sleep(1)
         | 
| 225 | 
            +
                            continue
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                        batch = []
         | 
| 228 | 
            +
                        while not movies_queue.empty() and len(batch) < batch_size:
         | 
| 229 | 
            +
                            try:
         | 
| 230 | 
            +
                                movie = movies_queue.get_nowait()
         | 
| 231 | 
            +
                                batch.append(movie)
         | 
| 232 | 
            +
                            except queue.Empty:
         | 
| 233 | 
             
                                break
         | 
| 234 |  | 
| 235 | 
            +
                        if not batch:
         | 
| 236 | 
            +
                            break
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 237 |  | 
| 238 | 
            +
                        logging.info(f"Обработка пакета из {len(batch)} фильмов...")
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                        for movie in batch:
         | 
| 241 | 
            +
                            embedding_string = f"Название: {movie['name']}\nГод: {movie['year']}\nЖанры: {movie['genresList']}\nОписание: {movie['description']}"
         | 
| 242 | 
            +
                            string_crc32 = calculate_crc32(embedding_string)
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                            # Проверяем существующий эмбеддинг
         | 
| 245 | 
            +
                            existing_embedding = get_embedding_from_db(conn, embeddings_table, "string_crc32", string_crc32, model_name)
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                            if existing_embedding is None:
         | 
| 248 | 
            +
                                embedding = encode_string(embedding_string)
         | 
| 249 | 
            +
                                embedding_crc32 = calculate_crc32(str(embedding.tolist()))
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                                if insert_embedding(conn, embeddings_table, movie['id'], embedding_crc32, string_crc32, embedding):
         | 
| 252 | 
            +
                                    logging.info(f"Сохранен эмбеддинг для '{movie['name']}'")
         | 
| 253 | 
            +
                                else:
         | 
| 254 | 
            +
                                    logging.error(f"Ошибка сохранения эмбеддинга для '{movie['name']}'")
         | 
| 255 | 
            +
                            else:
         | 
| 256 | 
            +
                                logging.info(f"Эмбеддинг для '{movie['name']}' уже существует")
         | 
| 257 | 
            +
                except Exception as e:
         | 
| 258 | 
            +
                    logging.error(f"Ошибка при обработке фильмов: {e}")
         | 
| 259 | 
            +
                finally:
         | 
| 260 | 
            +
                    conn.close()
         | 
| 261 | 
            +
                    processing_complete = True
         | 
| 262 | 
            +
                    logging.info("Обработка фильмов завершена")
         | 
| 263 |  | 
| 264 | 
            +
            def get_movie_embeddings(conn):
         | 
| 265 | 
             
                """Загружает все эмбеддинги фильмов из базы данных."""
         | 
| 266 | 
             
                movie_embeddings = {}
         | 
| 267 | 
             
                try:
         | 
|  | |
| 278 | 
             
                    logging.error(f"Ошибка при загрузке эмбеддингов фильмов: {e}")
         | 
| 279 | 
             
                return movie_embeddings
         | 
| 280 |  | 
| 281 | 
            +
            def search_movies(query, top_k=10):
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 282 | 
             
                """Выполняет поиск фильмов по запросу."""
         | 
| 283 | 
             
                global search_in_progress
         | 
| 284 | 
             
                search_in_progress = True
         | 
| 285 | 
            +
                start_time = time.time()
         | 
| 286 | 
            +
             | 
| 287 | 
             
                try:
         | 
| 288 | 
            +
                    conn = get_db_connection()
         | 
| 289 | 
            +
                    if conn is None:
         | 
| 290 | 
            +
                        return "<p>Ошибка подключения к базе данных</p>"
         | 
| 291 |  | 
| 292 | 
            +
                    query_crc32 = calculate_crc32(query)
         | 
| 293 | 
            +
                    query_embedding = get_embedding_from_db(conn, query_cache_table, "query_crc32", query_crc32, model_name)
         | 
| 294 |  | 
| 295 | 
            +
                    if query_embedding is None:
         | 
| 296 | 
            +
                        query_embedding = encode_string(query)
         | 
| 297 |  | 
| 298 | 
            +
                        try:
         | 
| 299 | 
            +
                            with conn.cursor() as cur:
         | 
| 300 | 
            +
                                cur.execute(f"""
         | 
| 301 | 
            +
                                    INSERT INTO {query_cache_table} (query_crc32, query, model_name, embedding)
         | 
| 302 | 
            +
                                    VALUES (%s, %s, %s, %s)
         | 
| 303 | 
            +
                                    ON CONFLICT (query_crc32) DO NOTHING
         | 
| 304 | 
            +
                                """, (query_crc32, query, model_name, query_embedding.tolist()))
         | 
| 305 | 
            +
                            conn.commit()
         | 
| 306 | 
            +
                            logging.info(f"Сохранен новый эмбеддинг запроса: {query}")
         | 
| 307 | 
            +
                        except Exception as e:
         | 
| 308 | 
            +
                            logging.error(f"Ошибка при сохранении эмбеддинга запроса: {e}")
         | 
| 309 | 
            +
                            conn.rollback()
         | 
| 310 |  | 
| 311 | 
            +
                    # Используем косинусное расстояние для поиска
         | 
| 312 | 
            +
                    try:
         | 
| 313 | 
            +
                        with conn.cursor() as cur:
         | 
| 314 | 
            +
                            cur.execute(f"""
         | 
| 315 | 
            +
                                WITH query_embedding AS (
         | 
| 316 | 
            +
                                    SELECT embedding
         | 
| 317 | 
            +
                                    FROM {query_cache_table}
         | 
| 318 | 
            +
                                    WHERE query_crc32 = %s
         | 
| 319 | 
            +
                                )
         | 
| 320 | 
            +
                                SELECT m.movie_id, 1 - (m.embedding <=> (SELECT embedding FROM query_embedding)) as similarity
         | 
| 321 | 
            +
                                FROM {embeddings_table} m, query_embedding
         | 
| 322 | 
            +
                                ORDER BY similarity DESC
         | 
| 323 | 
            +
                                LIMIT %s
         | 
| 324 | 
            +
                            """, (query_crc32, top_k))
         | 
| 325 | 
            +
             | 
| 326 | 
            +
                            results = cur.fetchall()
         | 
| 327 | 
            +
                        logging.info(f"Найдено {len(results)} результатов поиска.")
         | 
| 328 | 
            +
                    except Exception as e:
         | 
| 329 | 
            +
                        logging.error(f"Ошибка при выполнении поискового запроса: {e}")
         | 
| 330 | 
            +
                        results = []
         | 
| 331 |  | 
| 332 | 
            +
                    results_html = "<ol>"
         | 
| 333 | 
            +
                    for movie_id, similarity in results:
         | 
| 334 | 
            +
                        # Находим название фильма по ID
         | 
| 335 | 
            +
                        movie_title = None
         | 
| 336 | 
            +
                        for movie in movies_data:
         | 
| 337 | 
            +
                            if movie['id'] == movie_id:
         | 
| 338 | 
            +
                                movie_title = movie['name']
         | 
| 339 | 
            +
                                break
         | 
| 340 |  | 
| 341 | 
            +
                        if movie_title:
         | 
| 342 | 
            +
                            results_html += f"<li><strong>{movie_title}</strong> (Сходство: {similarity:.4f})</li>"
         | 
| 343 | 
            +
                    results_html += "</ol>"
         | 
| 344 | 
            +
             | 
| 345 | 
            +
                    search_time = time.time() - start_time
         | 
| 346 | 
            +
                    logging.info(f"Поиск выполнен за {search_time:.2f} секунд.")
         | 
| 347 | 
            +
             | 
| 348 | 
            +
                    return f"<p>Время поиска: {search_time:.2f} сек</p>{results_html}"
         | 
| 349 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 350 | 
             
                except Exception as e:
         | 
| 351 | 
            +
                    logging.error(f"Ошибка при выполнении поиска: {e}")
         | 
| 352 | 
            +
                    return "<p>Произошла ошибка при выполнении поиска.</p>"
         | 
| 353 | 
            +
             | 
| 354 | 
             
                finally:
         | 
| 355 | 
            +
                    if conn:
         | 
| 356 | 
            +
                        conn.close()
         | 
| 357 | 
             
                    search_in_progress = False
         | 
| 358 |  | 
| 359 | 
             
            # Запускаем обработку фильмов в отдельном потоке
         | 
| 360 | 
            +
            processing_thread = threading.Thread(target=process_movies)
         | 
| 361 | 
            +
            processing_thread.start()
         | 
| 362 |  | 
| 363 | 
             
            # Создаем интерфейс Gradio
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 364 | 
             
            iface = gr.Interface(
         | 
| 365 | 
            +
                fn=search_movies,
         | 
| 366 | 
            +
                inputs=gr.Textbox(lines=2, placeholder="Введите запрос для поиска фильмов..."),
         | 
| 367 | 
            +
                outputs=gr.HTML(label="Результаты поиска"),
         | 
| 368 | 
            +
                title="Семантический поиск фильмов",
         | 
| 369 | 
            +
                description="Введите описание фильма, который вы ищете, и система найдет наиболее похожие фильмы."
         | 
| 370 | 
             
            )
         | 
| 371 |  | 
| 372 | 
            +
            # Запускаем интерфейс
         | 
| 373 | 
             
            iface.launch()
         |