"""Конфигурация индексер-воркера.""" from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Настройки индексер-воркера.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # PostgreSQL POSTGRES_HOST: str = "postgres" POSTGRES_PORT: int = 5432 POSTGRES_DB: str = "antiplagiator" POSTGRES_USER: str = "antiplagiator" POSTGRES_PASSWORD: str = "changeme" # Redis REDIS_URL: str = "redis://redis:6379/0" # RabbitMQ RABBITMQ_URL: str = "amqp://guest:guest@rabbitmq:5672/" # MinIO MINIO_ENDPOINT: str = "minio:9000" MINIO_ACCESS_KEY: str = "minioadmin" MINIO_SECRET_KEY: str = "changeme" MINIO_BUCKET_DOCS: str = "documents" MINIO_BUCKET_STAGING: str = "staging" # Elasticsearch ELASTICSEARCH_URL: str = "http://elasticsearch:9200" # Настройки обработки текста FRAGMENT_WINDOW_WORDS: int = 200 # Размер окна для фрагментов FRAGMENT_OVERLAP_WORDS: int = 50 # Перекрытие фрагментов # Максимум хэшей Winnowing на документ. Winnowing рассчитан на ПОЛНОЕ # хранение отпечатков: обрезка теряет гарантию, что скопированный фрагмент # разделит отпечатки с источником (при 500 хранилось ~14% отпечатков статьи, # и локализованный поиск фрагментов почти не срабатывал). Держим высоким; # цена — размер таблицы fingerprints (~3-4К строк на статью). MAX_FINGERPRINTS_PER_DOC: int = 20000 # Порог уровня 1: % отпечатков фрагмента, найденных в источнике, чтобы # пометить фрагмент как скопированный EXACT_FRAGMENT_THRESHOLD: float = 40.0 # Скачивание полного текста статей (PDF по URL источника) FETCH_FULL_TEXT: bool = False # Включить обогащение полным текстом при заливке корпуса # Лить из OpenAlex только open-access работы (is_oa:true) — резко повышает # долю статей с доступным PDF (покрытие full-text ~8% → ~70-80%) INGEST_OPEN_ACCESS_ONLY: bool = True FULL_TEXT_TIMEOUT: float = 30.0 # Таймаут скачивания одного документа, сек FULL_TEXT_MAX_BYTES: int = 30 * 1024 * 1024 # Лимит размера скачиваемого файла (30 МБ) FULL_TEXT_MIN_CHARS: int = 500 # Минимум символов, иначе считаем извлечение неудачным EMBED_BATCH_SIZE: int = 64 # Размер пачки документов для диспатча эмбеддингов # Сколько документов массового источника пишется одной транзакцией. # Больше — меньше накладных расходов, но длиннее транзакция: 500 статей по # 2000 отпечатков это миллион строк за раз, и на таком объёме соединение # обрывалось. 150 — компромисс, проверенный на заливке Википедии. BULK_WRITE_BATCH: int = 150 # Бюджет времени одного прогона index.run_parser. RabbitMQ рвёт канал # consumer'а, не сделавшего ack за consumer_timeout (по умолчанию 1800с): # воркер падает, сообщение передоставляется, таск начинается заново — # бесконечный краш-луп (docs/DR-HA.md §6). Прогон закругляется раньше и # честно помечается partial: остаток дозаливается повторным запуском. PARSER_TIME_BUDGET_S: float = 1500.0 # Автоматически добавлять проверенные работы студентов в базу для сравнения # (как в коммерческих системах — Антиплагиат.ру/Turnitin ловят списывание у # предыдущих потоков именно так). Выключено по умолчанию: без ручной # модерации студент, перепроверивший тот же файл дважды, получал 100% # "плагиата" — против собственной же более ранней загрузки. L1/L2/L3 теперь # и так исключают source=user_submission из сравнения, так что включать это # обратно есть смысл только вместе с реальной защитой от self/cross-match. AUTO_APPROVE_SUBMISSIONS: bool = False # App ENVIRONMENT: str = "development" DEBUG: bool = False @property def database_url_sync(self) -> str: """Синхронный URL для SQLAlchemy.""" return ( f"postgresql+psycopg2://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}" f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" ) settings = Settings()