العودة إلى المدونة

بناء RAG باستخدام PostgreSQL و pgvector: دليل عملي خطوة بخطوة

فريق تحرير IM Host10 أكتوبر 20269 دقائق قراءة
بناء RAG باستخدام PostgreSQL و pgvector: دليل عملي خطوة بخطوة

تقنية RAG (التوليد المعزز بالاسترجاع) تعني أن تبحث في مستنداتك أنت عن أقرب الفقرات إلى سؤال المستخدم قبل أن يجيب النموذج اللغوي، ثم تضعها في الـ prompt، فتأتي الإجابة من بياناتك لا من ذاكرة النموذج. وإضافة pgvector تمنح PostgreSQL نوع عمود اسمه vector ومعاملات للبحث بالتشابه، فتعيش الفقرات وتمثيلاتها الرقمية (embeddings) وبياناتها الوصفية في قاعدة بيانات تعرف إدارتها أصلًا. وفي هذا الدليل: تفعيل الإضافة، والجداول، والإدخال، والبحث بمسافة cosine عبر <=>، وفهرس HNSW، والتصفية، وحجم السيرفر.

كيف يعمل RAG؟ مساران منفصلان

  • الإدخال (مرة لكل مستند): تقسيمه إلى مقاطع (chunks) ← تحويل كل مقطع إلى embedding ← تخزين النص والمتجه والبيانات الوصفية.

  • الاستعلام (مع كل سؤال): تحويل السؤال إلى embedding بالنموذج نفسه ← جلب أقرب المقاطع ← بناء prompt يضم هذه المقاطع ومصادرها ← النموذج اللغوي يجيب ويذكر المصدر.

جودة الإجابة تتحدد غالبًا في الاسترجاع: إذا لم يكن المقطع الصحيح ضمن أول خمس نتائج فلن يستخدمه النموذج.

لماذا pgvector بدل قاعدة بيانات متجهات منفصلة؟

  • نظام واحد: النص والصلاحيات والبيانات الوصفية والمتجه في الصف نفسه، والتصفية مجرد WHERE وJOIN عادية في SQL.

  • المعاملات (Transactions): احذف المستند فيحذف ON DELETE CASCADE مقاطعه معه، ولا تبقى متجهات يتيمة في خدمة أخرى.

  • أدوات تعرفها: pg_dump والأدوار والمراقبة تعمل كما هي.

  • حجم واقعي: كقاعدة تقريبية، سيرفر PostgreSQL واحد بموارد مناسبة مع HNSW يتعامل براحة مع عدة ملايين من المقاطع، وقاعدة المتجهات المستقلة تبرر تعقيدها عند عشرات الملايين.

الخطوة 1: تفعيل إضافة pgvector

CREATE EXTENSION IF NOT EXISTS vector;
SELECT extversion FROM pg_extension WHERE extname = 'vector';

اسم الإضافة داخل PostgreSQL هو vector رغم أن المشروع اسمه pgvector، وتُفعَّل مرة واحدة لكل قاعدة بيانات. وعلى Debian أو Ubuntu مع مستودع PostgreSQL الرسمي اسم الحزمة postgresql-16-pgvector (غيّر الرقم حسب إصدارك). والبحث التكراري المستخدم في الخطوة 6 يحتاج الإصدار 0.8.0 أو أحدث.

الخطوة 2: جداول تناسب نموذج الـ embeddings

الرقم n في vector(n) يجب أن يساوي حجم مخرجات نموذج الـ embeddings تمامًا، وإلا فشل الإدخال. أحجام شائعة: 1536 لنموذج text-embedding-3-small، و1024 للنماذج متعددة اللغات مثل multilingual-e5-large وbge-m3، و384 لنموذج all-MiniLM-L6-v2 المخصص أساسًا للإنجليزية. ولمحتوى عربي أو مختلط بين العربية والإنجليزية اختر نموذجًا متعدد اللغات واختبره على مستنداتك أنت.

CREATE TABLE documents (
  id         bigserial PRIMARY KEY,
  tenant_id  integer NOT NULL,
  title      text NOT NULL,
  source_url text,
  updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE chunks (
  id          bigserial PRIMARY KEY,
  document_id bigint NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
  tenant_id   integer NOT NULL,
  chunk_index integer NOT NULL,
  content     text NOT NULL,
  embedding   vector(1536) NOT NULL,
  UNIQUE (document_id, chunk_index)
);

CREATE INDEX chunks_tenant_idx ON chunks (tenant_id);

احتفظ بالنص الأصلي بجوار المتجه، فأنت تحتاجه في الـ prompt، وتحتاجه لإعادة توليد كل الـ embeddings إذا غيّرت النموذج يومًا. ونسخنا tenant_id إلى جدول chunks حتى لا تحتاج التصفية إلى JOIN.

الخطوة 3: إدخال الـ embeddings

تقرأ pgvector المتجه نصًا بين قوسين مربعين مثل '[0.12, -0.03, 0.88]'. وفي التطبيق استخدم حزمة pgvector للغة Python مع psycopg 3، فهي تحوّل مصفوفات NumPy تلقائيًا:

pip install "psycopg[binary]" pgvector numpy
import numpy as np
import psycopg
from pgvector.psycopg import register_vector

conn = psycopg.connect("postgresql://rag:password@localhost/rag", autocommit=True)
register_vector(conn)

def embed(texts):
    """Call your embedding model; return one list of 1536 floats per text."""
    raise NotImplementedError

def add_document(tenant_id, title, url, chunks):
    vectors = embed(chunks)  # call the API before opening the transaction
    with conn.transaction():
        doc_id = conn.execute(
            "INSERT INTO documents (tenant_id, title, source_url) "
            "VALUES (%s, %s, %s) RETURNING id",
            (tenant_id, title, url),
        ).fetchone()[0]
        with conn.cursor() as cur:
            cur.executemany(
                "INSERT INTO chunks (document_id, tenant_id, chunk_index, content, embedding) "
                "VALUES (%s, %s, %s, %s, %s)",
                [(doc_id, tenant_id, i, text, np.array(vec))
                 for i, (text, vec) in enumerate(zip(chunks, vectors))],
            )

الخيار autocommit=True مهم، فبدونه يفتح استعلام النوع داخل register_vector معاملة ضمنية وقد لا تُحفظ إدخالاتك أبدًا. وتوليد الـ embeddings يسبق المعاملة حتى لا يحجز استدعاء API بطيء الأقفال. وللتحميل الضخم فإن COPY أسرع كثيرًا من INSERT.

الخطوة 4: البحث بالتشابه عبر مسافة cosine

def search(tenant_id, question, k=5):
    q = np.array(embed([question])[0])
    return conn.execute(
        """
        SELECT c.content, d.title, d.source_url,
               1 - (c.embedding <=> %(q)s) AS similarity
        FROM chunks c
        JOIN documents d ON d.id = c.document_id
        WHERE c.tenant_id = %(tenant)s
        ORDER BY c.embedding <=> %(q)s
        LIMIT %(k)s
        """,
        {"q": q, "tenant": tenant_id, "k": k},
    ).fetchall()

المعامل <=> يحسب مسافة cosine: الصفر يعني الاتجاه نفسه، وكلما كبرت القيمة قلّ التشابه، لذلك تعطيك 1 - distance درجة التشابه. وتوفر pgvector أيضًا <-> للمسافة الإقليدية و<#> لسالب حاصل الضرب الداخلي. وإذا كان نموذجك يُخرج متجهات مُطبَّعة فالثلاثة ترتب النتائج بالترتيب نفسه، وcosine هو الخيار الآمن افتراضيًا. ولا يُستخدم الفهرس إلا إذا كان ORDER BY بالمعامل نفسه الذي بُني عليه الفهرس، تصاعديًا ومع LIMIT.

وفي الـ prompt أرفق عنوان كل مقطع ورابطه، واطلب من النموذج أن يجيب من السياق فقط وأن يقول إن الإجابة غير موجودة، واستبعد المقاطع الأقل من حد تشابه تضبطه على أسئلتك الحقيقية.

الخطوة 5: إنشاء فهرس HNSW

بدون فهرس يقارن PostgreSQL السؤال بكل الصفوف، وهذا البحث الدقيق مقبول حتى عشرات الآلاف من المقاطع. بعد ذلك أضف فهرس HNSW:

SET maintenance_work_mem = '2GB';
SET max_parallel_maintenance_workers = 3;

CREATE INDEX chunks_embedding_hnsw
  ON chunks USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

-- per session or per transaction (SET LOCAL): more candidates = better recall
SET hnsw.ef_search = 100;
  • m وef_construction (الافتراضي 16 و64): القيم الأعلى تبطئ البناء مقابل دقة أفضل.

  • hnsw.ef_search (الافتراضي 40) عدد المرشحين الذين يفحصهم كل استعلام، ارفعه للدقة واخفضه للسرعة.

  • ابنِ الفهرس بعد التحميل الضخم، واستخدم CREATE INDEX CONCURRENTLY على جدول يخدم مستخدمين. واجعل maintenance_work_mem أقل بوضوح من الذاكرة المتاحة في السيرفر.

  • حد الأبعاد: HNSW على النوع vector يدعم حتى 2000 بُعد، ولنماذج 3072 بُعدًا افهرس تحويلًا إلى halfvec.

-- only if the column is vector(3072); query with the same cast in ORDER BY
CREATE INDEX ON chunks USING hnsw ((embedding::halfvec(3072)) halfvec_cosine_ops);

تأكد من استخدام الفهرس بأمر EXPLAIN ANALYZE، ويجب أن ترى "Index Scan using chunks_embedding_hnsw". أما IVFFlat فيُبنى أسرع بذاكرة أقل، لكنه يحتاج بيانات قبل بنائه وتوازنه بين السرعة والدقة أسوأ غالبًا، لذلك HNSW هو الاختيار المعتاد.

الخطوة 6: التصفية بالبيانات الوصفية دون خسارة نتائج

الفهارس التقريبية تطبّق WHERE بعد مسح الفهرس. فمع ef_search بقيمة 40 يعيد الفهرس نحو 40 مرشحًا، ثم تحذف تصفية العميل (tenant) صفوف العملاء الآخرين، فقد تحصل على نتيجتين بدل خمس. الحلول بالترتيب:

-- pgvector 0.8.0+: keep scanning the index until enough rows pass the WHERE filter
SET hnsw.iterative_scan = relaxed_order;
  • البحث التكراري (0.8.0 وما بعده) يواصل المرور في الفهرس حتى تتوفر صفوف كافية تطابق الشرط، بحد أقصى hnsw.max_scan_tuples. الخيار relaxed_order قد يعيد النتائج بترتيب غير دقيق قليلًا، وstrict_order يحافظ على الترتيب.

  • العملاء الصغار يخدمهم فهرس B-tree على tenant_id جيدًا، إذ يستطيع PostgreSQL إجراء بحث دقيق على صفوفهم وحدها بسرعة ودقة كاملة.

  • فهارس HNSW الجزئية (... WHERE tenant_id = 7) تناسب عددًا قليلًا من العملاء الكبار جدًا، لا المئات.

تقسيم المستندات: هنا تُكسب جودة RAG أو تضيع

  • الحجم: كقاعدة تقريبية من 200 إلى 500 كلمة لكل مقطع، مع تداخل بين 10 و15%.

  • التقسيم حسب البنية: العناوين والفقرات، دون قطع جملة في منتصفها. وابدأ كل مقطع بعنوان المستند والقسم.

  • نموذج واحد للطرفين: المستندات والأسئلة تمر على نموذج الـ embeddings نفسه.

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

  • المصطلحات الدقيقة: الـ embeddings ضعيفة مع أكواد المنتجات وأرقام الفواتير والأسماء، فادمجها مع البحث النصي (full-text في PostgreSQL أو pg_trgm) واجمع القائمتين.

  • القياس: احتفظ بـ 30 إلى 50 سؤالًا حقيقيًا مع المقطع الذي يجب أن يظهر لكل منها، وراجع نسبة ظهوره في أول خمس نتائج بعد كل تعديل.

النسخ الاحتياطي والصيانة

يشمل pg_dump المتجهات، لكن الفهارس يُعاد بناؤها عند الاسترجاع وبناء HNSW يأخذ وقتًا، فجرّب استرجاعًا حقيقيًا. واحتفظ بالمستندات الأصلية لتعيد توليد الـ embeddings، وعند تعديل مستند استبدل مقاطعه في معاملة واحدة. ولباقي العناية يغطي دليلنا عن ضبط PostgreSQL ونسخه الاحتياطي وتأمينه على VPS إعدادات shared_buffers والنسخ التلقائي وصلاحيات الوصول.

ما حجم السيرفر المطلوب لنظام RAG على pgvector؟

  • مساحة كل متجه: 4 بايت × عدد الأبعاد + 8 بايت. عند 1536 بُعدًا يساوي ذلك نحو 6 KB، أي أن 100 ألف مقطع تشغل قرابة 600 MB من المتجهات والمليون نحو 6 GB، ويضيف فهرس HNSW حجمًا مماثلًا أو أكبر. والنوع halfvec يقسم حجم المتجه على اثنين.

  • الذاكرة: الاستعلامات سريعة حين يتسع الفهرس في الذاكرة. وكقاعدة تقريبية: 4 GB RAM لنحو 100 ألف مقطع بـ 1536 بُعدًا، و16 GB أو أكثر للمليون.

  • توليد الـ embeddings: عبر API أو بنموذج محلي صغير على دفعات. باقات IM HOST سيرفرات CPU وهذا يناسبها، أما نماذج التوليد الكبيرة محليًا فتحتاج GPU.

وللصورة الكاملة عن المعالج والذاكرة والتخزين حسب نوع العمل، راجع مقالنا عن متطلبات سيرفر استضافة تطبيقات الذكاء الاصطناعي. وتأتي استضافة وكلاء الذكاء الاصطناعي من IM HOST وعليها PostgreSQL مع pgvector مثبتة مسبقًا، إلى جانب Docker وPython وNode.js، مع root كامل ونسخ احتياطي يومي مجاني. ولتشغيل الوكيل الذي يستخدم هذا الاسترجاع على مدار الساعة، اتبع دليلنا عن تشغيل وكيل ذكاء اصطناعي على سيرفر 24 ساعة باستخدام Docker.

أسئلة شائعة

كم عدد الأبعاد المناسب لعمود vector؟

نفس حجم مخرجات نموذج الـ embeddings تمامًا، مثل 1536 لنموذج text-embedding-3-small أو 1024 لنموذج bge-m3. وتغيير النموذج يعني غالبًا حجم عمود جديدًا وإعادة توليد كل المقاطع.

هل pgvector سريعة بما يكفي لبيئة الإنتاج؟

لمعظم التطبيقات الصغيرة والمتوسطة نعم. فمع فهرس HNSW يتسع في الذاكرة، يستغرق الاسترجاع عادةً جزءًا صغيرًا من الوقت الذي يقضيه النموذج اللغوي في كتابة الإجابة. وفكّر في قاعدة متجهات مستقلة عند عشرات الملايين من المتجهات أو معدلات استعلام مرتفعة جدًا.

هل تعمل pgvector مع النصوص العربية؟

pgvector تخزن أرقامًا، فدعم اللغة يأتي من نموذج الـ embeddings. استخدم نموذجًا متعدد اللغات، وحافظ على توحيد النص بالطريقة نفسها، واختبر الاسترجاع بأسئلة عربية حقيقية قبل الإطلاق.

إذا أردت سيرفرًا يكون عليه PostgreSQL وpgvector جاهزين من اليوم الأول، فاطّلع على باقات استضافة وكلاء الذكاء الاصطناعي من IM HOST التي تبدأ من $8.99 شهريًا. استخدم مفاتيح API الخاصة بك للـ embeddings والنموذج اللغوي، ومهندسونا متاحون 24/7 إن توقف الإعداد عند أي خطوة.

المزيد من مدونتنا

اكتشف المزيد من الأدلة العملية ورؤى الخدمات من فريق IM Host.

عرض كل المقالات