""" سكربت بناء فهرس 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()