semantic_search_production / scripts /build_embeddings.py
shahdAI's picture
Upload scripts/build_embeddings.py with huggingface_hub
43fc034 verified
Raw
History Blame Contribute Delete
4.57 kB
"""
سكربت بناء فهرس 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()