File size: 4,567 Bytes
4a920c4
 
 
 
 
 
 
 
 
 
 
 
 
 
361ddf5
 
 
 
 
 
4a920c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361ddf5
4a920c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c41d0a
43fc034
 
 
0c41d0a
 
4a920c4
361ddf5
4a920c4
361ddf5
4a920c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""
سكربت بناء فهرس 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()