Spaces:
Sleeping
Sleeping
| """ | |
| سكربت بناء فهرس FAISS من المعجم العربي. | |
| المدخلات: | |
| data/lexicon_source.json | |
| صيغة الملف: قائمة JSON، كل عنصر فيها: | |
| { | |
| "word": "الكلمة العربية", | |
| "definition": "نص التعريف المستخدَم لحساب المتجه" | |
| } | |
| (يمكن أن تتكرر نفس الكلمة بعدة تعريفات/معاني مختلفة - كل سطر يصبح متجهاً مستقلاً) | |
| المخرجات: | |
| data/lexicon.index -> فهرس FAISS (IndexFlatIP على متجهات مطبَّعة = cosine) | |
| data/lexicon_words.json -> قائمة بنفس ترتيب الفهرس، كل عنصر بنصه الأصلي | |
| (بتشكيله الكامل) كما ورد في lexicon_source.json | |
| ملاحظة: المتجهات تُحسَب من نص التعريف بعد إزالة التشكيل (remove_tashkeel)، | |
| ليطابق طريقة معالجة استعلام المستخدم في app/search.py، لكن lexicon_words.json | |
| يُحفَظ بالنص الأصلي المُشكَّل دون أي تعديل. | |
| يُشغَّل مرة واحدة (أو عند تحديث المعجم): | |
| python -m scripts.build_embeddings | |
| """ | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import faiss | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| # السماح باستيراد app.config عند تشغيل السكربت مباشرة | |
| sys.path.append(str(Path(__file__).resolve().parent.parent)) | |
| from app import config # noqa: E402 | |
| from app.search import remove_tashkeel # noqa: E402 | |
| def load_lexicon_source() -> list[dict]: | |
| source_path = config.DATA_DIR / "lexicon_source.json" | |
| if not source_path.exists(): | |
| raise FileNotFoundError( | |
| f"ملف المعجم غير موجود: {source_path}\n" | |
| "يجب وضع ملف lexicon_source.json بصيغة:\n" | |
| '[{"word": "...", "definition": "..."}, ...]' | |
| ) | |
| with open(source_path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| if not isinstance(data, list) or not data: | |
| raise ValueError("lexicon_source.json يجب أن يكون قائمة غير فارغة") | |
| for item in data: | |
| if "word" not in item or "definition" not in item: | |
| raise ValueError("كل عنصر يجب أن يحتوي على 'word' و 'definition'") | |
| return data | |
| def build_index(lexicon: list[dict], model_path: str | Path) -> tuple[faiss.Index, list[dict]]: | |
| print(f"[1/3] تحميل نموذج الاسترجاع من: {model_path}") | |
| model = SentenceTransformer(str(model_path), device=config.DEVICE) | |
| definitions_for_embedding = [ | |
| f"الكلمة: {remove_tashkeel(item['word'])} " | |
| f"النوع: {item.get('pos_group', '')} " | |
| f"التعريف: {remove_tashkeel(item['definition'])}" | |
| for item in lexicon | |
| ] | |
| print(f"[2/3] حساب المتجهات لعدد {len(definitions_for_embedding)} تعريف ...") | |
| embeddings = model.encode( | |
| definitions_for_embedding, | |
| batch_size=64, | |
| show_progress_bar=True, | |
| convert_to_numpy=True, | |
| ).astype("float32") | |
| # تطبيع L2 لكي يصبح Inner Product = Cosine Similarity | |
| faiss.normalize_L2(embeddings) | |
| dim = embeddings.shape[1] | |
| print(f"[3/3] بناء فهرس FAISS (IndexFlatIP) بأبعاد {dim} ...") | |
| index = faiss.IndexFlatIP(dim) | |
| index.add(embeddings) | |
| return index, lexicon | |
| def main() -> None: | |
| lexicon = load_lexicon_source() | |
| model_path = config.RETRIEVAL_MODEL_PATH | |
| if not model_path.exists(): | |
| raise FileNotFoundError( | |
| f"نموذج الاسترجاع غير موجود في: {model_path}\n" | |
| "شغّلي scripts/download_assets.py أولاً، أو ضعي النموذج يدوياً في هذا المسار." | |
| ) | |
| index, words_meta = build_index(lexicon, model_path) | |
| config.DATA_DIR.mkdir(parents=True, exist_ok=True) | |
| faiss.write_index(index, str(config.FAISS_INDEX_PATH)) | |
| print(f"تم حفظ الفهرس: {config.FAISS_INDEX_PATH}") | |
| with open(config.LEXICON_WORDS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(words_meta, f, ensure_ascii=False, indent=2) | |
| print(f"تم حفظ قائمة الكلمات: {config.LEXICON_WORDS_PATH}") | |
| print(f"\nإجمالي العناصر في الفهرس: {index.ntotal}") | |
| print("اكتمل بناء الفهرس بنجاح ✅") | |
| if __name__ == "__main__": | |
| main() | |