"""ایندکس ترکیبی روی pgvector + BM25 در حافظه. بردارها در pgvector (دیتابیس اختصاصی چت‌بات) به‌صورت افزایشی همگام می‌شوند — فقط سطرهای جدید یا تغییر یافته امبد می‌شوند. BM25 چون سبک است در هر شروع از روی همان سطرها ساخته می‌شود و نیازی به کش دیسکی ندارد. """ import logging import time from rank_bm25 import BM25Okapi from . import content as content_mod from .config import settings from .persian import tokenize from . import vectorstore log = logging.getLogger(__name__) class SiteIndex: """حالت در حافظهٔ بازیابی — سندها (برای BM25) و وضعیت آماده بودن.""" def __init__(self) -> None: self.docs: list[dict] = [] self.bm25: BM25Okapi | None = None @property def ready(self) -> bool: return self.bm25 is not None and bool(self.docs) INDEX = SiteIndex() def sync_index(force_rebuild: bool = False) -> dict: """همگام‌سازی کامل: محتوا → قطعه → pgvector (افزایشی) → ساخت BM25. خروجی: آمار همگام‌سازی. خطای اتصال به دیتابیس بالا فرستاده می‌شود. """ start = time.time() vectorstore.ensure_schema() docs = content_mod.fetch_site_documents(settings.database_url) chunks = content_mod.chunk_documents(docs) stats = vectorstore.sync_chunks(chunks, force=force_rebuild) INDEX.docs = vectorstore.fetch_all_chunks() corpus_tokens = [tokenize(d["text"]) for d in INDEX.docs] INDEX.bm25 = BM25Okapi(corpus_tokens) if corpus_tokens else None log.info("Index ready in %.1fs (%d vectors, %d docs, %s)", time.time() - start, len(INDEX.docs), len(chunks), stats) return stats