PyDA Course
متقدم حرّ ⏱ 240 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

محرك بحث دلالي

machine-learningnumpyembeddingssearchcosine-similaritysentence-transformers

البحث بالكلمات المفتاحية حرفي: اكتب «محرك سيارة» فيبحث النظام عن هذين الرمزين بالضبط. البحث الدلالي كسول مع اللغة: اكتب «مركبة موتور» فيجب أن يجد ما يزال فقرة عن المحركات، لأنه يمثل المعنى بوصفه متجهًا في فضاء عالي الأبعاد تحل فيه الأفكار المتشابهة قريبًا من بعضها. في 2026 تعمل تلك الحيلة على نماذج تحويل صغيرة يمكنك تشغيلها في دفتر ملاحظات، فيتناسب خط الأنابيب كله في يديك: ضمِّن مجموعة مستندات في متجهات كثيفة، واحفظها في مصفوفة NumPy، ثم أجب عن استعلام بلغة طبيعية بحساب أي الفقرات المضمّنة أقرب في مسافة جيب التمام. يبني هذا المشروع ذلك المحرك من النهاية إلى النهاية، ثم يواجه الحد الصادق ، عندما تفشل اللمعة الدلالية ويفوز تطابق كلمة مفتاحية عادي على اسم علم ، ويريك كيف يجرّب هجينٌ أيَّ الأنظمة أنت فيه.

يفترض هذا أساسيات بايثون 101 بالإضافة إلى وحدتي NumPy وpandas في المساق. إنه اختياري وغير مصنّف؛ راجع المشاريع الواقعية للقائمة الكاملة.

🎯 ما ستفعله

  1. حمّل مجموعة صغيرة حقيقية من قطع المستندات وافحصها.
  2. ضمِّن كل قطعة في متجه كثيف بنموذج تحويل صغير.
  3. خزّن المتجهات في مصفوفة NumPy وطبّعها مرة واحدة.
  4. أجب عن استعلامات باللغة الطبيعية بترتيب تشابه جيب التمام مع تضمين الاستعلام.
  5. ابنِ هجين كلمات-مفتاحية-مقابل-دلالات واعثر على الاستعلام الذي يفوز فيه كل نهج.

أين تُشغّل هذا

محليًا مع uv هو المسار الأساسي للنموذج الصغير ، ينزّل sentence-transformers نموذجًا ~100 ميغابايت مرة واحدة، ثم يضمّن ويبحث على CPU بالمللي ثانية. يغطي uv add sentence-transformers numpy pandas كل شيء؛ أول تشغيل يجلب الأوزان، والتشغيلات اللاحقة تستخدم التخزين المؤقت.

يمنحك GitHub Codespaces التجربة نفسها: افتح codespaces.new/abderrahim-lectures/python-data-analysis-course وتعمل الأوامر نفسها في تبويب متصفح مقابل مجموعة نصية صغيرة مجمّعة.

يتولى Google Colab وKaggle Notebooks وBinder هذا المشروع أفضل من أي مشروع آخر في المساق ، يعمل تحويل صغير بسعادة على CPU المجاني في Colab/Kaggle (وأحيانًا CUDA)، والنموذج من صنف all-MiniLM-L6-v2 ينزّل تلقائيًّا، وحلقة التضمين→البحث كلها تظهر داخليًّا مع المتجهات مرئية. التحفظ الصادق الوحيد: تنزّل الأوزان عند أول تشغيل (بضع مئات ميغابايت)، وإذا كنت دون اتصال فلن يُحمَّل النموذج ، لذا أجزاء حساب المتجهات الخالص ما تزال تعمل بـnumpy الذي حسبته مسبقًا، لكن خطوة التضمين الحية تحتاج وصولًا شبكيًّا إلى Hugging Face.

Open In Colab Open In Kaggle Binder

الإعداد

أدوات، ومكتبة واحدة مع تنزيل نموذج، ومجموعة نصية صغيرة من قطع مستندات للبحث.

ثبّت uv وsentence-transformers

macOS / Linux (الطرفية):

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

أغلق وأعد فتح طرفيتك، ثم:

uv --version
mkdir semantic-search-engine && cd semantic-search-engine
uv init --bare
uv add sentence-transformers numpy pandas

تنزيل نموذج أول تشغيل (مرة واحدة):

# fetch_model.py
from sentence_transformers import SentenceTransformer
SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
print("model ready")
uv run python fetch_model.py

مجموعة نصية مصغرة

يشحن مستودع المساق مجموعة صغيرة من فقرات «حقائق عن الحيوانات» قصيرة ، ملموسة بما يكفي لتمييز الدلالات عن الكلمات المفتاحية:

# corpus.py
CORPUS = {
    0: "Dolphins communicate using clicks and whistles underwater.",
    1: "The tallest mountain on Earth is Mount Everest in Asia.",
    2: "Octopuses have three hearts and blue blood.",
    3: "Cats spend most of their day sleeping.",
    4: "Mount Kilimanjaro is a dormant volcano in Africa.",
    5: "Dogs are descendants of gray wolves, domesticated over thousands of years.",
}

لا بأس أن تبدّل نصّك الخاص (ملاحظات مساق، قصص) ، أي قطع قصيرة تصلح.

✅ قائمة التحقق

  • ✅ يطبع uv --version رقم نسخة؛ وsentence-transformers وnumpy وpandas مثبتة.
  • ✅ يطبع uv run python fetch_model.py model ready (بضع مئات ميغابايت مخزنة مؤقتًا عند أول تشغيل).
  • ✅ مجموعتك النصية قاموس بايثون لبضع سلاسل قطع قصيرة.

الخطوة 1: حمّل القطع وافحصها

قبل أن يلمس أي حساب نموذجًا، انظر إلى المادة الخام ، المجموعة النصية صغيرة عمدًا، لتستطيع معرفة كل قطعة عند الإبصار. عادة «اطبع بياناتك قبل أن تحولها» هي ما يفصل سكربتًا يثق بالنموذج عن سكربت يستطيع قراءة مدخلات النموذج.

👟 تلميح البداية : ابدأ باستيراد CORPUS، وبناء قائمتي ids وtexts المتوازيتين من مفاتيحه وقيمه، وطباعة عدد القطع مع نص كل قطعة قبل أي تشغيل تضمين.

# search.py
from corpus import CORPUS

ids = list(CORPUS.keys())
texts = list(CORPUS.values())
print(f"{len(ids)} chunks, {sum(len(t.split()) for t in texts)} words total")
for i, (cid, t) in enumerate(CORPUS.items()):
    print(f"{cid:>2}  {t[:70]}")

تهجير id → text هو المرجع الذي ستحتفظ به خلال كل خطوة لاحقة: التضمين هو الصيغة المقروءة آليًّا، لكن النص هو الجواب المواجه للإنسان، ويعيد محرك البحث الذي يظنه الشخص يريد قراءته. إبقاء ids وtexts قائمتَين متوازيتين (أو القاموس الذي بدأت به) هو الانضباط الذي يمنعك من إعادة «متجه 14.7» حين سأل المستخدم سؤالًا.

🎯 الناتج المتوقع : عدًّا (6 قطع، نحو 40 كلمة إجمالًا) وقائمة مرقّمة لسلاسل الفقرات ، المحتوى نفسه الذي ستبحث فيه في الخطوات 2–5.

🩹 إذا لم يعمل : إذا فشل الاستيراد، فـcorpus.py ليس على مسار الاستيراد ، شغّل من نفس الدليل، أو ضع CORPUS مباشرة في search.py. إذا أظهرت الطباعة أسطرًا أقل من المتوقع، فشرطة مائلة زائدة هربت سطرًا جديدًا بصمت ، حرفية CORPUS تحتاج معالجة \{؛ الاقتباس بـ""" هو الإصلاح المتين.

✅ قائمة التحقق

  • ✅ تطبع المجموعة النصية كاملة بأعداد صحيحة ثابتة للمعرّفات.
  • ✅ يمكنك تسمية، من الذاكرة، قطعة «مزعجة» (مفهومًا مشتركًا مثل mountains عبر 1 و4) لاختبار الدلالات لاحقًا.
  • ids وtexts متزامنتان (بنفس الترتيب) لبقية خط الأنابيب.

🤔 سؤال (أسئلة) سقراطي(ة)

  • تذكر قطعتان كلمة «mountain» (1 و4) لكنهما تصفان جبلين مختلفين. لا يستطيع بحث كلمات مفتاحية تمييزهما بذلك الرمز؛ ما الذي يجعلهما دلاليًّا مختلفتين، ولماذا ذلك التمييز هو بالضبط ما يفترض بالتضمينات التقاطه؟
  • المجموعة النصية صغيرة. ما السبب العملي للنمذجة الأولية على 6 جمل قبل التوسع إلى 6000 ، ما الثغرة التي تكشفها مجموعة من 6 مستندات ويدفنها 6000 مستند؟

الخطوة 2: ضمِّن كل قطعة

القفزة من كلمات إلى أرقام هي جوهر المشروع. يقرأ نموذج محوّل الجُمل كل قطعة ويخرج متجهًا كثيفًا ثابت الحجم (هنا 384 رقمًا) حيث تحل الجمل المتشابهة دلاليًّا قرب بعضها وتتباعد غير المرتبطة. «المعنى» يصبح هندسة: اتجاهًا في فضاء التضمين.

👟 تلميح البداية : ابدأ بتحميل النموذج مرة واحدة (SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")) واستدعاء model.encode(texts, normalize_embeddings=True, convert_to_numpy=True) ، ثم اطبع X.shape ومعيار الصف الأول.

# search.py (continued)
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

def embed(texts: list[str]) -> np.ndarray:
    return model.encode(texts, normalize_embeddings=True, convert_to_numpy=True)

X = embed(texts)
print("embedding matrix:", X.shape)          # (6, 384) in this model
print("row norm[0]     :", round(float(np.linalg.norm(X[0])), 4))

model.encode(..., normalize_embeddings=True) يعيد مصفوفة (n_chunks, 384)، كل صف منها متجه وحدة (معيار ≈ 1.0). التطبيع مرة واحدة مقدمًا يعني أن كل تشابه لاحق هو جيب تمام ، ومع متجهات الوحدة ينهار تشابه جيب التمام إلى جداء نقطي بسيط، فيكون حساب البحث كله في الخطوة 3 X @ q واحدة. الرقم 384 البعدي اختيار تصميمي للنموذج؛ لا تضبط ذلك، بل تختار نموذجًا، لكنك تصمم حول مخرجه.

🎯 الناتج المتوقع : embedding matrix: (6, 384) وrow norm[0]: 1.0 (ضمن تقريب الفاصلة).

🩹 إذا لم يعمل : إذا تعطل تنزيل النموذج أو أخطأ، فوصول الشبكة إلى Hugging Face محجوب ، خطوة fetch_model.py من الإعداد يجب أن تنجح أولًا؛ خلف وكيل، وجّه HF_ENDPOINT إلى مرآة. إذا لم تكن X هي (6, 384)، فقد وجّهت نصوصًا خاطئة إلى encode ، convert_to_numpy=True يضمن مصفوفة؛ قائمة قوائم ضالة تعني أنك فوّت تحويل المصفوفة.

✅ قائمة التحقق

  • X.shape == (6, 384) ومعيار كل صف ≈ 1.0.
  • ✅ تنتج قطعتان متشابهتان دلاليًّا متجهين قريبين ، تحقق أن np.dot(X[1], X[4]) (زوج «mountain») أعلى من np.dot(X[1], X[2]).
  • ✅ يمكنك قول ما الذي يمنحك إياه «متجه الوحدة» لاحقًا (جيب التمام == الجداء النقطي).

🤔 سؤال (أسئلة) سقراطي(ة)

  • يفرض normalize_embeddings=True طول وحدة، فيُسقط «كم قالت هذه الرسالة» ويبقى «إلى أي اتجاه تشير» فقط. متى يكون الطول إشارة ذات معنى تود إبقاءها (استعلام يطلب إجابة طويلة متعرجة بدل موجزة)؟ ولماذا يفضل البحث أصلًا الاتجاه-فقط؟
  • يضمّن النموذج نفسه كلمة وسياقها الكامل. قطعة عن «python script» وقطعة عن «python snake» ، نفس الرمز، تضمينان مختلفان، لأن النموذج ينظر إلى الكلمات المحيطة. تتبّع ما يعنيه ذلك لمجال بكلمات مفتاحية غامضة، وأين يفشل بصمت (متجانسات لم يفصّلها النموذج جيدًا).

الخطوة 3: ابحث بتشابه جيب التمام

المحرك، في سطريّ حساب: ضمِّن استعلام المستخدم، ثم رتّب كل قطعة مخزنة بتشابه جيب التمام معه. لأن الخطوة 2 طبّعت الصفوف، فإن جيب التمام والجداء النقطي شيء واحد، وX @ q يعيد درجة لكل قطعة في خطوة موجهة واحدة. الترتيب هو المنتج كله.

👟 تلميح البداية : ابدأ بكتابة search(query, X, texts, ids, k) التي ترمز الاستعلام تمامًا مثل المستندات، وتسجّل كل قطعة بـX @ q، وتعيد أعلى k عبر np.argsort(scores)[::-1].

# search.py (continued)

def search(query: str, X: np.ndarray, texts: list[str], ids: list[int],
           k: int = 3) -> list[tuple[int, float, str]]:
    q = model.encode([query], normalize_embeddings=True, convert_to_numpy=True)[0]
    scores = X @ q
    order = np.argsort(scores)[::-1][:k]
    return [(ids[i], float(scores[i]), texts[i]) for i in order]

for q in ["an animal that lives in the sea", "a very tall landform", "sleeping pet"]:
    print(f"\nquery: {q!r}")
    for cid, score, text in search(q, X, texts, ids):
        print(f"   {score:>0.3f}  [{cid}] {text[:60]}")

يتبع الاستعلام مسار التضمين المماثل للمستندات ، نفس النموذج، نفس التطبيع ، فيعيش متجه الاستعلام في نفس الفضاء الدلالي، وX @ q هو جداء نقطة تشابه جيب التمام. np.argsort(scores)[::-1] يرتّب تنازليًّا ويقطع أعلى k. المردود ظاهر في أول استعلام: يجب أن «an animal that lives in the sea» يعتمد قطعة الدلفين (0) وقطعة الأخطبوط (2) ، رغم أن لا قطعة منهما تحتوي كلمتي «sea» أو «animal». تلك دلالات: أعطت السحابة معنى، ورتّب الجداء النقطي بها.

🎯 الناتج المتوقع : بالنسبة لـ«an animal that lives in the sea»، تكون الإصابات العليا قطعتَي الدلفين (0) والأخطبوط (2) بدرجات أعلى بكثير من الجبلين (1، 4)؛ وبالنسبة لـ«sleeping pet»، يجب أن تتصدر قطعة القط (3) ، لأن كلمتي استعلامك («sea» و«pet») لا تظهران في أي مستند، فالمطابقة دلالية خالصة.

🩹 إذا لم يعمل : إذا انتهى استعلام البحر عند قطع الجبال، فلم يعمّم النموذج بالطريقة التي رجوتها ، جرّب استعلامًا أكثر اصطلاحية («marine creature»)؛ تتباين جودة التضمين مع الصياغة. إذا كانت كل الدرجات 0، فتطبيع الاستعلام يختلف عن تطبيع المجموعة ، كلاهما يجب أن يستخدم normalize_embeddings=True. إذا أعاد np.argsort عدم تطابق شكل IndexError، فلن يكون X وq كلاهما (..., 384) ، يصطدم خط أنابيب نموذج خاطئ (مثلًا نموذج مختلف ينتج بُعدًا مختلفًا)؛ أعِد فحص شكل المصفوفة من الخطوة 2.

✅ قائمة التحقق

  • ✅ يرتّب استعلام البحر الدلفين + الأخطبوط فوق الجبال ، معنى، لا رموز.
  • ✅ الدرجات في [0, 1] (متجهات وحدة)، وترتيب الرتبة ثابت عبر التشغيلات.
  • ✅ يمكنك الإشارة إلى السطر بالضبط الذي يؤدي البحث (X @ q + argsort).

🤔 سؤال (أسئلة) سقراطي(ة)

  • البحث كله X @ q بعد التطبيع. لو لم تطبّع، ما الكميتان اللتان تخلطهما (مقدار المستندات × مقدار الاستعلام) ولماذا يشوّهان مرئيًّا ترتيب مستند معلوماتي طويل مقابل قصير لنفس الموضوع؟
  • argsort(scores)[::-1] يفرز تصاعديًّا ثم يقلب. ما الفرق الدقيق بين ذلك وscores.argsort()[: -(k+1) : -1] ، ولماذا يعمل كلاهما هنا؟ (فكّر فيما يفعله «عكس مصفوفة مرتبة تصاعديًّا» بالتعادلات.)

الخطوة 4: هجين ، كلمات مفتاحية حين تهم

البحث الدلالي قوي لكنه ليس قاهرًا: عندما يحتوي الاستعلام اسم علم أو رمزًا دقيقًا نادرًا، قد تكون المطابقة المعجمية أكثر موثوقية من تخمين النموذج. يمزج محرك حقيقي الاثنين ، درجة كلمات مفتاحية (رموز متطابقة/متداخلة) مدمجة مع درجة دلالية ، ويكشف الزرّ لترى كل جانب يفوز. تبني هذه الخطوة الهجين وتواجه الفشل الصادق.

👟 تلميح البداية : ابدأ بكتابة keyword_score(query, text) التي تحسب رموز الاستعلام الموجودة في القطعة مقسومة على عدد رموز القطعة، ثم ادمجها في hybrid(...) كـalpha * sem + (1 - alpha) * kw.

# search.py (continued)
import re

TOK = re.compile(r"[a-z0-9]+")

def keyword_score(query: str, text: str) -> float:
    q = set(TOK.findall(query.lower()))
    t = TOK.findall(text.lower())
    return sum(1 for w in t if w in q) / max(1, len(t))

def hybrid(query: str, X: np.ndarray, texts: list[str], ids: list[int],
           alpha: float = 0.5, k: int = 3) -> list[tuple[int, float, str]]:
    q = model.encode([query], normalize_embeddings=True, convert_to_numpy=True)[0]
    sem = X @ q
    kw = np.array([keyword_score(query, t) for t in texts])
    blended = alpha * sem + (1 - alpha) * kw
    order = np.argsort(blended)[::-1][:k]
    return [(ids[i], float(blended[i]), texts[i]) for i in order]

for q in ["Mount Everest", "an animal that lives in the sea"]:
    print(f"\nquery: {q!r}")
    for alpha in (0.0, 1.0):
        print(f"  alpha={alpha}")
        for cid, s, text in hybrid(q, X, texts, ids, alpha=alpha):
            print(f"     {s:>0.3f}  [{cid}] {text[:50]}")

يحسب keyword_score كم من رموز القطعة يظهر في الاستعلام، مطبّعًا بعدد رموز القطعة ، إشارة معجمية ساذجة لكن أمينة. يمزجها hybrid مع الدرجات الدلالية (مطبَّعة أصلًا [0,1]، فيبقى مجموع alpha قابِلًا للمقارنة) ويرتّب. الدراما في الاستعلامين: بالنسبة لـ«Mount Everest» (اسم علم قد يكون النموذج رآه لكن مطابق الكلمات يثبّته بالضبط)، يجب أن يؤدي بحث الكلمات المفتاحية alpha=0 حسنًا بقدر الذراع الدلالية أو أحسن؛ وبالنسبة للتعبير المرادف «animal that lives in the sea»، تكون الكلمات المفتاحية عاجزة (تلك الكلمات في لا مستند) ولا تعمل إلا دلالات alpha=1. وظيفة الهجين إمساك الاثنين ، وفجوة الدرجة المطبوعة هي دليلك على أي نظام أنت فيه.

🎯 الناتج المتوقع : بالنسبة لـ«Mount Everest»، تتصدر قطعة إيفرست (1) ذراعي alpha معًا، لكن الفجوة بين المرتبة 1 والمرتبة 2 (كليمنجارو، 4) عادةً أقطع للكلمات المفتاحية (alpha=0)؛ وبالنسبة لاستعلام البحر، لا يجد alpha=0 شيئًا (الكلمات في لا مستند)، بينما يرتّب alpha=1 الدلفين + الأخطبوط أولًا ، النظامان مرئيان في جدول واحد.

🩹 إذا لم يعمل : إذا أعاد alpha=0 لاستعلام البحر شيئًا (قطعة برمز مشترك عشوائي مثل «a»)، فمطابق keyword_score يطابق أدوات/كلمات توقف ، أضف مُرشِّح كلمات توقف صغيرًا، أو اقبله تحيزًا معروفًا للنموذج ودع الفجوة تعلّمك. إذا رُتّب «Mount Everest» أسوأ عند alpha=1 منه عند 0، فالمحوّل يخفّض وزن الأسماء النادرة ، بالضبط الفشل الذي يرتقعه بحث الكلمات المفتاحية، وهو الخلاصة: يمزج alpha، ولا ذراع تصيب دائمًا.

✅ قائمة التحقق

  • ✅ استعلامات الاسم العلم ترتّب جيدًا عبر الكلمات المفتاحية؛ واستعلامات المرادفات عبر الدلالات فقط.
  • ✅ يغيّر alpha الترتيب مرئيًّا عبر صنفي الاستعلام.
  • keyword_score محصور في [0, 1]، بنفس مدى sem، فالمزيج مقارنةً بأمثال.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يفترض المزيج أن الدرجتين تعيشان على [0, 1]. يقسم keyword_score على طول القطعة حتى لا تفوز الوثائق الطويلة بالحجم. لكن ما الذي يكلّفه التطبيع بـmax(1, len) عند استحقاق قطعة من 3 كلمات للتطابق ، وأليس «كم مصطلح استعلام يظهر هنا» غير المطبَّع أحيانًا إشارة تجارية أفضل؟
  • لا قيمة «صحيحة» عامة لـalpha. ما الطريقة التجريبية لاختيارها لمجموعتك الخاصة ، مجموعة صغيرة من الاستعلامات ذات أجوبة صحيحة معروفة، ثم اختر alpha التي ترتّبها صحيحةً أكثر الأحيان ، وفخ ضبط alpha على نفس الاستعلامات التي تبلّغ عنها؟

الخطوة 5: شخّص متى ينكسر

الخطوة الأخيرة صدق فكري: محرك بحث لا يريك إلا الفائزين يخفي اللحظات التي يخطئ فيها. تصطاد هذه الخطوة الفشل عمدًا ، استعلامًا تكون مرتبته العليا قريبة دلاليًّا لكنها خاطئة واقعيًّا، أو إعادة صياغة يقرؤها النموذج خطأ ، وتصنّفه، لتخرج فاهمًا القدرة والحد معًا.

👟 تلميح البداية : ابدأ بكتابة show_all(query, X, texts, ids) ، رمّز الاستعلام، وسجّل بـX @ q، واطبع كل قطعة بدرجتها تنازليًّا بدل أعلى k فقط.

# search.py (continued)

def show_all(query: str, X: np.ndarray, texts: list[str], ids: list[int]) -> None:
    q = model.encode([query], normalize_embeddings=True, convert_to_numpy=True)[0]
    scores = X @ q
    order = np.argsort(scores)[::-1]
    print(f"\nquery: {query!r}")
    for i in order:
        print(f"   {scores[i]:>0.3f}  [{ids[i]}] {texts[i][:60]}")

show_all("the tallest mountain in Africa", X, texts, ids)
show_all("a creature with three hearts", X, texts, ids)

يطبع show_all كل قطعة بدرجتها بدل أعلى k فقط، لتستطيع رؤية الترتيب كاملًا وتحديد حالة الحد. مسبار «أعلى جبل في أفريقيا» هو الفخ: النموذج ربما ربط «mountain» بقوة بـإيفرست (1) من بيانات تدريبه، فقد تكون المرتبة 1 قطعة إيفرست رغم أن الجواب الواقعي الصحيح كليمنجارو (4). ذلك التشخيص الصادق ، تضمينات تقيس الترابط الإحصائي، لا الحقيقة الأرضية ، وتسميتها هي المهارة الحقيقية.

🎯 الناتج المتوقع : بالنسبة لـ«the tallest mountain in Africa»، ترتيب كامل قد يضع إيفرست (1) فوق كليمنجارو (4) ، توضيح مثالي أن هذا المحرك يقيس الصلة بعبارة “أعلى جبل”، لا التحقق من الواقعة. يجب أن تصدّر «a creature with three hearts» الأخطبوط (2) نظيفًا.

🩹 إذا لم يعمل : إذا رتّب مسبار أفريقيا كليمنجارو أولًا غير المتوقع، فتقدير المخاطر كان خاطئًا لمصلحتك ، اختار النموذج السياق صحيحًا؛ تلك خطوة التباين، وإعادة التشغيل بعبارة مختلفة قليلًا («قمة عالية جدًّا في أفريقيا») ستعيده عادةً إلى الفخ. إذا كان كل شيء خطًّا مستويًا نظيفًا (كل قطعة ≈ 0.5)، فمجموعتك متجانسة أكثر من اللازم ، بدّل مواضيع أكثر تميزًا حتى تنتشر الدرجات.

✅ قائمة التحقق

  • ✅ يطبع show_all كل قطعة بدرجة، لا أعلى 3 فقط.
  • قد يضع مسبار أفريقيا إيفرست فوق كليمنجارو ، ويمكنك شرح لماذا (ترابط ≠ واقع).
  • ✅ يمكنك صياغة حد هذا المحرك في كلمة واحدة: يبحث عن نص مرتبط، لا حقيقة.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يرمّز النموذج «أعلى جبل في أفريقيا» مع روابط باقية قوية إلى إيفرست من التدريب. هل ذلك ثغرة في النموذج، أم سمة نماذج اللغة الإحصائية يجب أن تصحّحها طبقة تحقق من الحقائق؟ جادل الطرفين باختصار.
  • رُتّبت كل قطعة، لكن كون الدرجة العليا الأعلى لا يعني أنها جيدة ، ما تزال فقرة مرتبطة لكن خاطئة قد تسجّل 0.8. ماذا ستضيف العتبة (لا جواب إذا كانت الدرجة القصوى < θ)، وما خطرها عندما لا يكون الجواب الصحيح في المجموعة أصلًا؟

⚠️ مآزق شائعة

  • نسيان التطبيع. بلا normalize_embeddings=True على المستندات والاستعلام معًا، يتحول جيب التمام إلى جداء نقطي خام يصرخ «مستند طويل، درجة أعلى» ويشوّه الترتيب بالطول. طبّع مرة واحدة، في كل مكان.
  • نماذج غير متطابقة. تضمين بنموذج والاستعلام بنموذج آخر (بُعد مختلف، فضاء مختلف) يخطئ الترتيب بصمت. رمّز المستندات والاستعلامات بنفس نسخة SentenceTransformer.
  • إعادة متجهات لا نصوص. بحث يعيد «قطعة 3، درجة 0.9» تجربة مستخدم مكسورة. أبقِ id → text متزامنًا (قائمتين متوازيتين) حتى تُرسَم كل إصابة مرتبة إلى شيء يستطيع إنسان قراءته.
  • معاملة الدرجات الدلالية كحقيقة. تضمينات ترمّز الترابط الإحصائي، لا الحقائق ، قد ترتّب «أعلى جبل في أفريقيا» إيفرست لأن النموذج تعلّم أن إيفرست شهير. أضف طبقة تحقق (فحص كلمات مفتاحية أو استرجاع فوق حقل واقعي) لأي شيء حساس واقعيًّا.
  • ضبط alpha على الاستعلام لا المجموعة. اختيار α لإطراء استعلام عرضي واحد إفراط في الملاءمة. اختره بمجموعة محجوزة من أزواج (استعلام، إصابة متوقعة) وبلّغ عن معدل الإصابة ، نفس الانضباط الذي جعل الهجين موثوقًا.

ما بنيته للتو

محرك بحث دلالي عامل: ضمّنت مجموعة نصية صغيرة في مصفوفة NumPy بحجم 6×384، وطبّعت الصفوف حتى أصبح جيب التمام جداءً نقطيًّا X @ q واحدًا، ورتّبت استعلامات اللغة الطبيعية بالتشابه، ودمجت ذراع كلمات مفتاحية بـalpha قابلًا للضبط، ثم ، أهون جزء ، نظرت إلى الترتيب الكامل وسمّيت مواضع سذاجته بالضبط. الأفكار المنقولة تتجاوز البحث بكثير: عادة «طبّع مرة واحدة، فيصبح الهندسة حسابًا»، وحد «اربط، لا تحقق» الذي يشحن به كل نموذج تضمين، وانضباط طباعة كل درجاتك لا الفائزين فقط.

شغّل نسخة أكمل دون أي إعداد محلي

examples/semantic-search-engine/ في مستودع المساق يحزم مجموعة نصية أغنى، ووحدة التضمين-والبحث، ودفترًا يحمّل ويضمّن ويرتّب ويمزج ويعرض انتشار الدرجات كاملًا داخليًّا. استنسخ المستودع، أو افتحه في GitHub Codespaces، وشغّل الخطوات الخمس من النهاية إلى النهاية.

إلى أين تذهب من هنا

  • مجموعة نصية أكبر: اقرأ مستندات من مجلد (pathlib.glob) وقسّمها إلى فقرات قبل التضمين ، اللعبة ذات الستة عناصر تصبح فهرسًا حقيقيًا.
  • أصِرّ على الفهرس: وفّر X بـnp.save وحمّلها دون إعادة تضمين، حتى تكون البدايات الباردة قراءة ملف، لا استدعاء نموذج.
  • واجهة برمجة: لفّ search في نقطة /search?q=... من FastAPI تعيد JSON {id, score, text} ، الدالة نفسها، الآن متاحة عبر HTTP.
  • تحقق واقعي من الإصابة العليا: أضف إعادة فحص كلمات مفتاحية (keyword_score من الخطوة 4) بوصفه حارسًا قبل وصول المرتبة 1 إلى مستخدم، مسدًّدًا فجوة «الترابط ليس واقعًا».

شارك مشروعك مع الصف

ضمّنت مجموعة، أو وجدت فوزًا دلاليًّا، أو صوّرت فشل اسم علم؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون، وREADME يرشد إلى إضافة مشروعك عبر طلب سحب من البداية إلى النهاية: الشوكة والفرع والالتزام وفتح الـPR. لا خبرة git مسبقة مفترضة.

مرحبًا بك في كتابة Python خارج المتصفح. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.