الخطوة 4 في RAG: إدخال المعرفة إلى قاعدة بيانات المتجهات

ستحمّل الآن مقالات أخبار Clark College ومستندات الدعم إلى pgvector.

الخطوة 1: تعرّف على البيانات النموذجية

يحتوي الملف advanced/rag/sample_data.py على نحو 30 مستندًا عن Clark College:

  • مقالات إخبارية: معرض CTE، ومسابقة Geomatics، ومهرجان Sakura، وغيرها.
  • خدمات الدعم: جلسات الدعم التعليمي، والمساعدات المالية، وPenguin Pantry، والإرشاد، وغيرها.

يبدو كل مستند هكذا:

{
    "title": "Tutoring Services at Clark College",
    "content": "Clark College offers free, friendly, and supportive tutoring...",
    "source": "https://www.clark.edu/campus-life/student-support/",
    "category": "support",
}

هذه مستندات نموذجية — ينبغي أن تستبدلها ببياناتك الخاصة.

الخطوة 2: قطّع المستندات

المستندات أطول من أن تُضمَّن كقطعة واحدة، لذا نقسّمها إلى مقاطع (chunks):

النص الأصلي: "Clark College offers free tutoring in business, English,
            math, science, and general education classes. Online
            eTutoring is also available..."

المقطع 1: "Clark College offers free tutoring in business, English, math,"
المقطع 2: "math, science, and general education classes. Online eTutoring"
المقطع 3: "Online eTutoring is also available to currently enrolled..."

كل مقطع نحو 300 كلمة، مع تداخل 50 كلمة حتى لا يضيع أي سياق عند الحدود.

الخطوة 3: أنشئ سكربت الإدخال

أنشئ الملف advanced/rag/knowledge_base.py:

from sentence_transformers import SentenceTransformer
from sample_data import DOCUMENTS

CHUNK_SIZE = 300
CHUNK_OVERLAP = 50
embedder = SentenceTransformer("all-MiniLM-L6-v2")


def chunk_text(text, chunk_size=CHUNK_SIZE, overlap=CHUNK_OVERLAP):
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = start + chunk_size
        chunk = " ".join(words[start:end])
        chunks.append(chunk)
        start += chunk_size - overlap
    return chunks


def get_all_chunks(documents=None):
    if documents is None:
        documents = DOCUMENTS
    all_chunks = []
    for doc in documents:
        chunks = chunk_text(doc["content"])
        for i, chunk in enumerate(chunks):
            all_chunks.append({
                "title": doc["title"],
                "content": chunk,
                "source": doc["source"],
                "category": doc.get("category", "general"),
                "chunk_index": i,
            })
    return all_chunks


def index_documents(db, DocumentModel, documents=None):
    chunks = get_all_chunks(documents)
    for chunk in chunks:
        vec = embedder.encode(chunk["content"]).tolist()
        doc = DocumentModel(
            title=chunk["title"],
            content=chunk["content"],
            source=chunk["source"],
            category=chunk["category"],
            embedding=vec,
        )
        db.session.add(doc)
    db.session.commit()
    return len(chunks)


if __name__ == "__main__":
    from app import create_app, db
    from app.models import Document

    app = create_app()
    with app.app_context():
        db.create_all()
        count = index_documents(db, Document)
        print(f"Indexed {count} chunks from {len(DOCUMENTS)} documents")

الخطوة 4: شغّل عملية الإدخال

تأكد من أن PostgreSQL وpgvector يعملان:

cd aiproject/advanced
docker compose -f docker-compose.rag.yml up -d db

ثم شغّل عملية الإدخال:

cd aiproject/backend
DATABASE_URL="postgresql://aiproject:aiproject@localhost:5432/aiproject" \
    python -m advanced.rag.knowledge_base

الناتج المتوقع:

Indexed 87 chunks from 30 documents

الخطوة 5: تحقّق من البيانات

from app import create_app, db
from app.models import Document

app = create_app()
with app.app_context():
    docs = Document.query.limit(3).all()
    for d in docs:
        print(f"[{d.category}] {d.title}: {d.content[:80]}...")

المتوقع:

[support] Tutoring Services at Clark College: Clark College offers free tutoring...
[news] CTE Showcase 2026: Clark College welcomed 20 high schools...
[support] Disability Access Center: The Disability Access Center assists...

كيف تضيف بياناتك الخاصة

الخيار 1: عدّل sample_data.py

أضف مدخلات جديدة إلى قائمة DOCUMENTS:

{
    "title": "Your Document Title",
    "content": "Full text of your document...",
    "source": "https://example.com",
    "category": "your-category",
}

ثم أعد تشغيل knowledge_base.py لفهرسة البيانات الجديدة.

الخيار 2: حمّل من ملف JSON

import json
with open("my_documents.json") as f:
    documents = json.load(f)

ينبغي أن يكون ملف JSON قائمة من الكائنات ذات الحقول title وcontent وsource وcategory.

الخيار 3: كاشط ويب (اختياري)

استخدم requests وbeautifulsoup4 لكشط صفحات الويب. راجع advanced/rag/challenges.md للتفاصيل.

اكتب اختبارات للتضمينات

اختبر أن نموذج التضمين ينتج المخرجات الصحيحة:

أنشئ الملف advanced/rag/tests/test_rag_engine.py:

import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from rag_engine import embed_text, build_rag_context


def test_embed_text_returns_384_dimensions():
    vec = embed_text("Clark College offers free tutoring")
    assert len(vec) == 384
    assert all(isinstance(v, float) for v in vec)


def test_embed_text_consistent_output():
    vec1 = embed_text("tutoring services")
    vec2 = embed_text("tutoring services")
    assert vec1 == vec2  # المدخل نفسه ← المتجه نفسه


def test_build_rag_context_formats_correctly():
    results = [{"title": "Test", "content": "Hello world"}]
    context = build_rag_context(results)
    assert "[Test]" in context
    assert "Hello world" in context

شغّلها:

cd aiproject
python -m pytest advanced/rag/tests/ -v

تتحقق هذه الاختبارات من: - أن أحجام التضمينات صحيحة (384 بُعدًا) - أن التضمينات حتمية (المدخل نفسه ← المتجه نفسه) - أن دالة بناء السياق تنسّق النتائج بشكل صحيح

اختبر عملك (يدويًا)

from sentence_transformers import SentenceTransformer
from app.models import Document
from app import create_app, db

model = SentenceTransformer("all-MiniLM-L6-v2")
app = create_app()

with app.app_context():
    query = "Where can I get free tutoring?"
    query_vec = model.encode(query).tolist()

    results = (
        db.session.query(Document)
        .order_by(Document.embedding.cosine_distance(query_vec))
        .limit(3)
        .all()
    )

    for r in results:
        sim = 1 - r.embedding.cosine_distance(query_vec)
        print(f"[{sim:.2f}] {r.title}")
        print(f"  {r.content[:100]}...")

المتوقع: أن تحتل المستندات المتعلقة بالدعم التعليمي أعلى الترتيب.