"""نرمال‌سازی متن فارسی و توکن‌سازی برای جست‌وجوی کلیدواژه (BM25). بدون وابستگی سنگین — فقط استانداردسازی نویسه‌ها، حذف اعراب و نیم‌فاصله‌های مشکلی و شکستن کلمه‌های مرکب روی نیم‌فاصله برای بهبود بازیابی. """ import re # نویسه‌های عربی که در متن فارسی اشتباهاً استفاده می‌شوند _CHAR_MAP = { "\u064a": "\u06cc", # ي → ی "\u0643": "\u06a9", # ك → ک "\u0623": "\u0627", # أ → ا "\u0625": "\u0627", # إ → ا "\u0671": "\u0627", # ٱ → ا "\u0629": "\u0647", # ة → ه "\u0624": "\u0648", # ؤ → و "\u0626": "\u06cc", # ئ → ی "\u0640": "", # ـ (کشیده) حذف } # اعراب و علامت‌های اضافی _DIACRITICS = re.compile(r"[\u064b-\u0652\u0670\u0653-\u0655\u0640]") # ارقام فارسی و عربی → ASCII _DIGITS = str.maketrans({ "\u06f0": "0", "\u06f1": "1", "\u06f2": "2", "\u06f3": "3", "\u06f4": "4", "\u06f5": "5", "\u06f6": "6", "\u06f7": "7", "\u06f8": "8", "\u06f9": "9", "\u0660": "0", "\u0661": "1", "\u0662": "2", "\u0663": "3", "\u0664": "4", "\u0665": "5", "\u0666": "6", "\u0667": "7", "\u0668": "8", "\u0669": "9", }) # واژه‌های دستوری پرتکرار — در جست‌وجو بی‌معنا هستند STOPWORDS = frozenset(""" و در به از که با این آن است هست بود بودن شد شدن شود کرد کردن کرده کند کنی برای را های ها هایی هم نیز اما یا اگر تا بر روی زیر بین پس قبل بعد همه هر چه چیزی کس کسی کجا چرا چگونه چطور چند یکی دو سه ما شما آنها ایشان من تو او کدام کی چی چه‌قدر چقدر می نخواهد خواهد باشد باشند هستیم هستند بودم بودیم دارد دارند داشته داد دهد بله خیر آیا فقط نیز خیلی بسیار کمی دیگر دیگری همان همین وقتی زمانی وقتیکه اساساً لطفاً میشه میکنه هستن هستم """.split()) # علائم نگارشی فارسی/لاتین — قبل از توکن‌سازی حذف می‌شوند (نیم‌فاصله جزو اینها نیست) _PUNCT_RE = re.compile(r"[،؛؟!,;:\?\"'()\[\]{}«»….*#@&%+=/\\|~^$\u066d\u066c\u066b]+") # توکن: حروف/ارقام فارسی و لاتین + نیم‌فاصله در میان کلمه _TOKEN_RE = re.compile(r"[\w\u0600-\u06ff\u200c]+") def normalize(text: str) -> str: """نویسه‌های غیراستاندارد عربی/فارسی را به شکل یکدست فارسی می‌برد.""" if not text: return "" out = [] for ch in text: out.append(_CHAR_MAP.get(ch, ch)) text = "".join(out) text = _DIACRITICS.sub("", text) text = text.translate(_DIGITS) # فاصله‌های تکراری را یکی کن — نیم‌فاصله عمداً حفظ می‌شود (می‌شود) text = re.sub(r"[ \t\r\n]+", " ", text) return text.strip() def tokenize(text: str) -> list[str]: """توکن‌های نرمال‌شده برای BM25 — کلمه‌های مرکب روی نیم‌فاصله هم جدا ایندکس می‌شوند.""" text = _PUNCT_RE.sub(" ", normalize(text).lower()) tokens = [] for raw in _TOKEN_RE.findall(text): if raw in STOPWORDS or len(raw) < 2: continue tokens.append(raw) # «می‌شود» → می + شود (فراخوانی بیشتر برای بازیابی) if "\u200c" in raw: for part in raw.split("\u200c"): if len(part) >= 2 and part not in STOPWORDS: tokens.append(part) return tokens