دليل تعليمي لضبط أداء pgvector HNSW في Postgres 18 لبيئة الإنتاج ٢٠٢٦

١٦ مايو ٢٠٢٦

pgvector HNSW Postgres 18 Production Tuning Tutorial 2026

ملخص

إنشاء فهرس HNSW جاهز للإنتاج في pgvector 0.8.2 على PostgreSQL 18 يتطلب ستة بيانات SQL، وثلاثة إعدادات GUC، وحيلة تكميم (quantization) واحدة — لكن الإعدادات الافتراضية مهيأة للعروض التوضيحية، وليس لضغط العمل الحقيقي لديك. يشرح هذا الدليل الإعداد باستخدام Docker، وبناء فهرس HNSW بالتوازي، واختبار قيم ef_search مقابل دقة الاسترجاع (recall) المقاسة، وتقليل مساحة التخزين للنصف باستخدام halfvec، واستخدام المسح التكراري في pgvector 0.8 للحفاظ على دقة استرجاع عالية تحت فلاتر WHERE الانتقائية. تمت كتابة كل كود برمجى بناءً على النسخة pgvector/pgvector:0.8.2-pg18؛ قم بنسخها ولصقها بالترتيب وستتطابق خطط EXPLAIN مع الهيكل الموصوف في كل خطوة.

القيمة الافتراضية لـ hnsw.ef_search هي 40، والقيمة الافتراضية لـ m هي 16، والقيمة الافتراضية لـ ef_construction هي 64. هذه هي نقطة البداية الصحيحة — وهي أيضاً السبب في أن معظم المنشورات التي تدعي أن "pgvector بطيء" خاطئة: لم يقم أحد بتعديل هذه القيم. بنهاية هذا الدليل، ستعرف بالضبط كيفية تحريك كل منها لمجموعة بياناتك وما هي التكلفة من حيث زمن الاستجابة، والذاكرة، ودقة الاسترجاع.

ما ستتعلمه

  • كيفية تثبيت pgvector 0.8.2 على PostgreSQL 18 باستخدام صورة Docker محددة ولماذا تجعل ثغرة CVE-2026-3172 هذا الإصدار هو الحد الأدنى غير القابل للتفاوض.
  • كيفية ربط معاملات HNSW وهي m و ef_construction و ef_search بدقة الاسترجاع ووقت البناء وزمن استجابة الاستعلام.
  • كيفية بناء فهرس HNSW بالتوازي باستخدام max_parallel_maintenance_workers وذاكرة maintenance_work_mem بحجم مناسب.
  • كيفية إجراء اختبار دقة الاسترجاع مقابل ef_search واختيار القيمة المناسبة لحركة البيانات لديك.
  • كيف يقلل تكميم halfvec مساحة تخزين الفهرس للنصف مع فقدان ضئيل في دقة الاسترجاع على تضمينات OpenAI ذات الـ 1536 بُعداً.
  • كيف ينقذ hnsw.iterative_scan = 'relaxed_order' استعلامات المتجهات المفلترة التي قد تعيد نتائج فارغة لولا ذلك.
  • كيفية صيانة فهارس HNSW في بيئة الإنتاج باستخدام REINDEX INDEX CONCURRENTLY و VACUUM.

المتطلبات الأساسية

  • Docker Desktop 4.30+ (أو Docker Engine 24+) لحاوية Postgres.
  • psql 16+ على الجهاز المضيف (يأتي العميل مع معظم عمليات تثبيت Postgres وصيغة libpq في Homebrew).
  • حوالي 4 جيجابايت من ذاكرة RAM المتاحة إذا اتبعت قسم البناء المتوازي.
  • حوالي 15 دقيقة، وإما مفتاح OpenAI API لنموذج text-embedding-3-small (المستخدم هنا كمثال قياسي بـ 1536 بُعداً، بتكلفة ~0.02 دولار لكل مليون توكن مدخل1) أو أي مصدر آخر لمتجهات float بـ 1536 بُعداً.

الإصدارات المحددة المستخدمة في الدليل:

المكونالإصدارملاحظات
PostgreSQL18.3إصدار خارج الدورة بتاريخ 26-02-20262
pgvector0.8.2صدر في 26-02-2026؛ يعالج ثغرة CVE-2026-31723
صورة Dockerpgvector/pgvector:0.8.2-pg18متعددة البنيات (amd64/arm64) على Docker Hub4

الخطوة 1: تشغيل PostgreSQL 18 + pgvector 0.8.2 في Docker

صورة pgvector/pgvector الرسمية هي صورة أساسية لـ PostgreSQL مبنية مسبقاً مع الامتداد (extension) مجمع داخلها بالفعل. استخدم الوسم الدقيق 0.8.2-pg18 — حيث أن pg18 وحده يتتبع "الأحدث" وسيتغير يوم شحن pgvector 0.8.3.

أنشئ ملف docker-compose.yml في مجلد فارغ:

services:
  db:
    image: pgvector/pgvector:0.8.2-pg18
    container_name: pgvector-tuning
    environment:
      POSTGRES_USER: pgv
      POSTGRES_PASSWORD: pgv
      POSTGRES_DB: vectors
    ports:
      - "5433:5432"
    shm_size: 2gb
    command: >
      postgres
        -c maintenance_work_mem=2GB
        -c max_parallel_maintenance_workers=4
        -c shared_buffers=1GB
        -c effective_cache_size=3GB
    volumes:
      - pgvector_data:/var/lib/postgresql/data

volumes:
  pgvector_data:

ثلاثة أشياء تستحق التنويه قبل تشغيل docker compose up:

  1. المنفذ 5433 يتجنب التعارض مع أي نسخة Postgres محلية لديك بالفعل على المنفذ 5432.
  2. shm_size: 2gb يتطابق مع maintenance_work_mem. تشارك عمليات بناء HNSW المتوازية الذاكرة عبر /dev/shm؛ وبدون shm_size مطابق، ستتوقف العمال (workers) بأخطاء OOM غامضة في نهاية عملية بناء قد تستغرق ساعات.
  3. max_parallel_maintenance_workers=4 يرفع الحد الأقصى لكل عملية بناء من القيمة الافتراضية لـ Postgres وهي 2. السقف الفعلي محدود أيضاً بـ max_worker_processes (الافتراضي 8) وعدد vCPU لديك.

ابدأ الحاوية وقم بتفعيل الامتداد:

docker compose up -d
docker exec -it pgvector-tuning psql -U pgv -d vectors -c "CREATE EXTENSION IF NOT EXISTS vector;"
docker exec -it pgvector-tuning psql -U pgv -d vectors -c "SELECT extname, extversion FROM pg_extension WHERE extname='vector';"

المخرجات المتوقعة:

 extname | extversion
---------+------------
 vector  | 0.8.2
(1 row)

إذا رأيت 0.7.x أو 0.8.0/0.8.1، فأنت تستخدم نسخة معرضة للخطر. ثغرة CVE-2026-3172 هي تجاوز سعة المخزن المؤقت (buffer overflow) بتقييم CVSS 8.1 يتم تحفيزه أثناء بناء فهارس HNSW المتوازية ويمكن أن يسرب بيانات من علاقات غير مرتبطة أو يتسبب في تعطل الخادم.3 الحل هو الترقية إلى 0.8.2؛ والحل المؤقت الموثق إذا لم تتمكن من الترقية فوراً هو تعطيل عمال الصيانة المتوازيين تماماً (SET max_parallel_maintenance_workers = 0)، مما يزيل المسبب على حساب بناء فهارس أبطأ بكثير. يستخدم هذا الدليل البناء المتوازي عمداً، لذا فإن الإصدار 0.8.2 هو الحد الأدنى.

الخطوة 2: المخطط، التضمينات، وفئة العامل الصحيحة

لبقية الدليل، سنستخدم جدول documents مصمم على غرار مجموعة بيانات RAG حقيقية: مفتاح أساسي، عمود بيانات وصفية (metadata) للبحث المفلتر، وعمود vector بـ 1536 بُعداً يطابق مخرجات text-embedding-3-small من OpenAI.1

CREATE TABLE documents (
  id          bigserial PRIMARY KEY,
  source      text NOT NULL,
  language    text NOT NULL,
  content     text NOT NULL,
  embedding   vector(1536) NOT NULL,
  created_at  timestamptz NOT NULL DEFAULT now()
);

لإجراء اختبار أداء محلي، قم بتوليد مجموعة بيانات اصطناعية مكونة من 100 ألف صف من متجهات وحدة عشوائية بـ 1536 بُعداً. التضمينات الحقيقية ليست عشوائية، لكن متجهات الوحدة الموزعة بانتظام هي اختبار ضغط جيد لهيكل الفهرس — فهي تنتج رسماً بيانياً في "أسوأ الحالات" (بدون تجمعات) يكشف أخطاء الضبط فوراً:

INSERT INTO documents (source, language, content, embedding)
SELECT
  'synthetic-' || (gs % 10),
  CASE WHEN gs % 4 = 0 THEN 'ar' ELSE 'en' END,
  'doc ' || gs,
  (
    SELECT array_agg((random() - 0.5)::real)::vector
    FROM generate_series(1, 1536)
  )
FROM generate_series(1, 100000) AS gs;

تستغرق عملية الإدخال هذه من دقيقة إلى دقيقتين على كمبيوتر محمول حديث وتنتج بضع مئات من الميغابايت من البيانات — حيث يهيمن عمود التضمين على حجم الجدول.

الآن القرار الحاسم: أي عامل مسافة نستخدم؟ يوفر pgvector أربعة عوامل لـ vector وأربعة مماثلة لـ halfvec:

العاملالمسافةفئة عامل الفهرس لـ vectorمتى يستخدم
<=>جيب التمام (Cosine)vector_cosine_opsواجهات برمجة تطبيقات LLM/التضمين الحديثة (OpenAI, Cohere, Voyage) — مخرجاتها معيارية؛ جيب التمام هو الخيار القياسي.
<->L2 (Euclidean)vector_l2_opsبيانات المستشعرات الفيزيائية، وأي مكان يهم فيه المقدار.
<#>الناتج الداخلي السالبvector_ip_opsإذا كانت متجهاتك معيارية بـ L2 بالفعل، فهذا هو البديل الأسرع لجيب التمام لأنه يتم تخطي خطوة المعايرة.
<+>L1 (Manhattan)vector_l1_opsنادر؛ مفيد لقواميس الميزات المتفرقة وبعض خوارزميات التجميع.

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

في هذا الدليل سنستخدم جيب التمام، وهو الخيار الافتراضي الصحيح لتضمينات OpenAI.

الخطوة 3: بناء فهرس HNSW مع عمال متوازيين

بناء الفهرس (index) البسيط بجملة واحدة يقوم بالمهمة الصحيحة ولكنه يمنع عمليات الكتابة على الجدول طوال مدة البناء، والتي قد تصل إلى عشرات الدقائق لبضعة ملايين من الصفوف. في بيئة التشغيل الحقيقية (production)، استخدم دائماً CREATE INDEX CONCURRENTLY — حيث يدعم pgvector ذلك لكل من HNSW و IVFFlat.

بارامترات HNSW الافتراضية من ملف README الخاص بـ pgvector5 هي:

البارامترالافتراضيما يتحكم فيه
m16الحد الأقصى لعدد الاتصالات ثنائية الاتجاه لكل طبقة في الرسم البياني (graph). أكبر = استرجاع (recall) أعلى، فهرس أكبر، بناء أبطأ.
ef_construction64حجم قائمة المرشحين الديناميكية أثناء البناء. أكبر = رسم بياني بجودة أفضل، بناء أبطأ.
hnsw.ef_search40حجم قائمة المرشحين وقت الاستعلام. أكبر = استرجاع أعلى، زمن استجابة (latency) أعلى للاستعلام.

بالنسبة للتضمينات (embeddings) ذات الـ 1536 بُعداً، استقر المجتمع التقني على m=16 (لا تغيره إلا إذا واجهت مشكلة في الاسترجاع بعد استنفاد محاولات تعديل ef_search) و ef_construction=128–200 كنقطة بداية معقولة لبيئة التشغيل.

بناء الفهرس:

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

CREATE INDEX CONCURRENTLY documents_embedding_hnsw_idx
  ON documents
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 128);

لا يمكن تشغيل CREATE INDEX CONCURRENTLY داخل كتلة معاملة (transaction block)، مما يعني أنه لا يمكنك فتح psql BEGIN أولاً. قم بتشغيله مباشرة. في مجموعة البيانات الاصطناعية المكونة من 100 ألف صف أعلاه، يكتمل البناء في بضع دقائق على لابتوب حديث مع max_parallel_maintenance_workers=4؛ وعادةً ما يؤدي رفع عدد العمال (workers) أكثر (ومطابقة max_worker_processes معها) إلى تقليل ذلك الوقت للنصف أو أكثر. يشير اختبار أداء البناء المتوازي من Neon إلى تسريع يصل إلى ~30 ضعفاً عند 1 مليون متجه مع 8 عمال على maintenance_work_mem بحجم سخي.6

وضعان للفشل يجب التخطيط لهما:

  • maintenance_work_mem صغير جداً: بمجرد أن يتجاوز الرسم البياني في الذاكرة الميزانية المحددة، يصدر pgvector تنبيهاً NOTICE: hnsw graph no longer fits into maintenance_work_mem وينتقل من بناء الرسم البياني في الذاكرة إلى بنائه على القرص، وهو أمر أبطأ بشكل كبير. راقب pg_stat_progress_create_index.tuples_done لملاحظة انهيار المعدل في الثانية — هذا هو العرض. الحل هو رفع maintenance_work_mem قبل إعادة المحاولة.
  • /dev/shm صغير جداً: يتواصل العمال المتوازيون عبر الذاكرة المشتركة. في Docker، يكون الافتراضي لـ /dev/shm هو 64 ميجابايت. بدون shm_size مطابق، يتوقف البناء عند مرحلة الدمج مع خطأ could not resize shared memory segment. ملف docker-compose.yml أعلاه يضبط shm_size: 2gb، وهو ما يطابق maintenance_work_mem. اجعلهما متساويين دائماً.

تحقق من وجود الفهرس وتحقق من حجمه:

SELECT
  indexname,
  pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_indexes
JOIN pg_class c ON c.relname = indexname
JOIN pg_index ON pg_index.indexrelid = c.oid
WHERE tablename = 'documents';

المتوقع لـ 100 ألف صف عند 1536 بُعداً مع m=16: يستقر الفهرس في نطاق المئات العالية من الميجابايت — الجزء الأكبر من الحجم هو حمولة التضمين المكررة في عقد رسم HNSW البياني بالإضافة إلى قوائم الاتصال لكل عقدة. بالقياس على 1 مليون متجه عند 1536 بُعداً، يستقر فهرس HNSW على vector عادةً بالقرب من 8 جيجابايت.7 هذا الرقم يوجه كل قرار يتعلق بالذاكرة ستتخذه من الآن فصاعداً — يجب أن يتناسب الفهرس مع shared_buffers (أو على الأقل في ذاكرة التخزين المؤقت لصفحات نظام التشغيل التي يعكسها effective_cache_size) وإلا ستنهار سرعة الاستعلامات بمجرد طرد صفحات الرسم البياني من الذاكرة.

الخطوة 4: ضبط ef_search — مسح الاسترجاع مقابل زمن الاستجابة

أكثر تمرين ضبط مفيد في pgvector هو مسح hnsw.ef_search مقابل الاسترجاع عند تثبيت m و ef_construction. وهو أيضاً التمرين الذي لا يشرحه أي دليل تعليمي تقريباً.

الآلية: يتحكم hnsw.ef_search في عدد المتجهات المرشحة التي يحتفظ بها البحث في قائمة الأولويات الخاصة به أثناء التنقل في الرسم البياني. القيمة الافتراضية 40 هي قيمة متحفظة؛ يمكنك رفعها إلى 1000.5 يرتفع الاسترجاع بشكل مطرد مع زيادة ef_search؛ بينما يرتفع زمن استجابة الاستعلام بشكل أسرع من الخطى لأن كل مرشح يكلف عملية حساب مسافة مقابل متجه الاستعلام.

اختر متجه استعلام، وحدد "الحقيقة المطلقة" (ground truth) باستخدام مسح تسلسلي شامل (brute-force)، وقم بقياس استرجاع HNSW@K عند كل قيمة لـ ef_search:

-- استعلام مرجعي: خذ تضمين الصف رقم 12345 وابحث عن أقرب 50 جاراً له.
\set query_id 12345

-- الحقيقة المطلقة: مسح دقيق، بدون فهرس.
SET enable_indexscan = off;
CREATE TEMP TABLE gt_top50 AS
SELECT id FROM documents
ORDER BY embedding <=> (SELECT embedding FROM documents WHERE id = :query_id)
LIMIT 50;
SET enable_indexscan = on;

-- الاسترجاع عند 50 عندما تكون ef_search = 10.
SET LOCAL hnsw.ef_search = 10;
SELECT COUNT(*) AS hits FROM (
  SELECT id FROM documents
  ORDER BY embedding <=> (SELECT embedding FROM documents WHERE id = :query_id)
  LIMIT 50
) ann
WHERE id IN (SELECT id FROM gt_top50);

كرر الكتلة الثانية مع ضبط SET LOCAL hnsw.ef_search على 20، 40، 80، 160، 320، و 640. ارسم الاسترجاع على محور ووقت تنفيذ EXPLAIN (ANALYZE, BUFFERS) على المحور الآخر. الشكل الذي ستراه، في كل مجموعة بيانات كبيرة بما يكفي لاستحقاق استخدام HNSW، هو نفسه: يرتفع الاسترجاع بحدة بين ef_search=10 والافتراضي 40، ثم يرتفع ببطء أكثر ليصل إلى ~95-99% عند ef_search في نطاق 80-160، ثم يستقر بعد ذلك. يرتفع زمن الاستجابة بشكل أسرع من الخطى مع ef_search لأن كل مرشح إضافي هو عملية حساب مسافة جديدة مقابل متجه الاستعلام.

الخلاصة العملية: هناك دائماً "منعطف" في مكان ما في نطاق 80-200 حيث تكلف كل نقطة استرجاع إضافية زمن استجابة أكبر بشكل غير متناسب. اختر أصغر قيمة لـ ef_search تحقق هدف الاسترجاع الخاص بك في استعلامات تمثيلية؛ هذا هو إعداد بيئة التشغيل الخاص بك. غالباً ما تحتوي التضمينات الحقيقية على هيكل مجمع يتنقل فيه HNSW بشكل جيد، مما يدفع الاسترجاع للأعلى عادةً عند قيم ef_search متوسطة — قد يكون المنعطف الخاص بك أقل مما يقترحه اختبار المتجهات العشوائية الاصطناعية.

لإجراء التغيير على مستوى المجموعة (يتطلب صلاحيات superuser)، استخدم ALTER SYSTEM متبوعاً بإعادة تحميل الإعدادات — حيث يتم رفض كل من ALTER ROLE ... SET hnsw.ef_search و ALTER DATABASE ... SET hnsw.ef_search مع خطأ permission denied to set parameter 'hnsw.ef_search' بسبب طريقة تسجيل pgvector للـ GUC.8

ALTER SYSTEM SET hnsw.ef_search = 80;
SELECT pg_reload_conf);

أو — وهو المفضل لكود التطبيق — اضبطه لكل استعلام داخل معاملة:

BEGIN;
SET LOCAL hnsw.ef_search = 160;
SELECT id, content
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
COMMIT;

يعد SET LOCAL هو النمط الصحيح لتطبيق يحتوي على حركة مرور مختلطة — حيث يمكن لنقطة نهاية بحث دقيقة مع ef_search = 160 وشريط جانبي "المزيد من هذا القبيل" مع ef_search = 40 التعايش في نفس تجمع الاتصالات دون تسريب الحالة عبر الطلبات.

الخطوة 5: تكميم halfvec — نصف مساحة التخزين، نفس الاسترجاع

أعلى حركة ضبط من حيث عائد الاستثمار (ROI) في معظم أعباء عمل التضمين الحديثة هي تبديل نوع العمود من vector إلى halfvec. يقوم halfvec بتخزين كل بُعد في دقة نصف IEEE 16-bit بدلاً من float 32-bit. تنخفض مساحة التخزين إلى النصف، ويتضاعف حد أبعاد HNSW من 2000 إلى 4000، وفي تضمينات OpenAI ذات الـ 1536 بُعداً، يُقال إن التغيير في الاسترجاع يكاد يكون مطابقاً للدقة الكاملة.9

أضف عمود halfvec وفهرساً مطابقاً:

ALTER TABLE documents ADD COLUMN embedding_h halfvec(1536);

UPDATE documents
SET embedding_h = embedding::halfvec(1536);

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

CREATE INDEX CONCURRENTLY documents_embedding_h_hnsw_idx
  ON documents
  USING hnsw (embedding_h halfvec_cosine_ops)
  WITH (m = 16, ef_construction = 128);

تفصيلان مهمان:

  • اسم فئة العمليات (op-class) هو halfvec_cosine_ops، وليس vector_cosine_ops — استخدام الاسم الخاطئ مع عمود halfvec سيؤدي إلى فشل إنشاء الفهرس بدلاً من التراجع الصامت، وهو وضع فشل جيد.
  • تتم عملية التحويل ::halfvec(1536) صفاً بصف؛ في مجموعة بيانات صغيرة تكتمل في ثوانٍ، ولكن في ملايين الصفوف يمكن أن تهيمن على نافذة الهجرة. بالنسبة لمجموعات البيانات الأكبر، قم بإجراء التحويل على دفعات (أو قم بذلك داخل معاملة واحدة مع حلقة LIMIT/OFFSET) أو طبقه كجزء من مسار الإدخال الأولي بحيث تصل الصفوف الجديدة في شكل halfvec.

أعد قياس مساحة التخزين:

SELECT
  indexname,
  pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_indexes
JOIN pg_class c ON c.relname = indexname
JOIN pg_index ON pg_index.indexrelid = c.oid
WHERE tablename = 'documents';

يجب أن ترى فهرس halfvec يأتي بحجم أصغر بكثير من نظيره vector. تشير أرقام جوناثان كاتز المرجعية إلى فجوة تبلغ حوالي 8 جيجابايت مقابل 3 جيجابايت لمجموعة بيانات بحجم 1 مليون × 1536 بُعدًا7 — أي تقليل بنسبة 60% تقريبًا في الفهرس، مدفوعًا بحمولات متجهة أصغر بنسبة 50% داخل عقد الرسم البياني بالإضافة إلى كثافة ذاكرة تخزين مؤقت أفضل بمجرد صغر صفحات الفهرس.10 إذا كنت تقوم بتشغيل نظام متعدد المستأجرين على نسخة Postgres واحدة وتخزن عشرات الملايين من التضمينات (embeddings)، فهذا هو الفرق بين "الحاجة إلى نسخة أقوى" و "الملاءمة داخل shared_buffers."

إذا كانت التضمينات الخاصة بك تأتي من نموذج يخرج بالفعل قيمًا منخفضة الدقة (على سبيل المثال، بعض نماذج Matryoshka مفتوحة المصدر)، فيمكنك الذهاب إلى أبعد من ذلك إلى التكميم القياسي (int8) أو التكميم الثنائي باستخدام bit_hamming_ops — قدم pgvector 0.7 الفهرسة على نوع bit حتى 64,000 بُعد.11 بالنسبة لمعظم أعباء عمل التضمين الكثيف الحديثة، فإن halfvec هو الخيار الأمثل.

الخطوة 6: البحث المتجه المفلتر والمسح التكراري

أصعب مشكلة في pgvector لتصحيح أخطائها في بيئة التشغيل ليست زمن الانتقال — بل هي مجموعات النتائج الفارغة عندما تضيف جملة WHERE إلى استعلام متجه. قبل الإصدار 0.8، كان المخطط يجلب أفضل ef_search مرشحين من فهرس HNSW ثم يطبق الفلتر الخاص بك فوقهم، مما يعني أن استعلامًا مثل "ابحث عن أقرب 10 مستندات باللغة العربية" يمكن أن يعيد 0 أو صفين حتى لو كان هناك مئات المستندات المطابقة في مجموعة البيانات.12 قدم pgvector 0.8.0 المسح التكراري للفهرس (iterative index scans) خصيصًا لإصلاح ذلك — حيث يستمر الفهرس في جلب المرشحين حتى يتم استيفاء الفلتر أو استنفاد ميزانية مسح الصفوف.13

ثلاثة أوضاع، يتم التحكم فيها بواسطة GUC الخاص بـ hnsw.iterative_scan (الافتراضي هو off):

الوضعالدلالاتمتى يُستخدم
off (افتراضي)السلوك الأصلي: أخذ ef_search مرشحين، والفلترة بعدها.افتراضي متوافق مع الإصدارات السابقة؛ جيد عندما تكون الفلاتر غير انتقائية (>50%).
strict_orderتوسيع مجموعة المرشحين تكراريًا مع الحفاظ على ترتيب المسافة الدقيق.عندما يجب عليك الحفاظ على ترتيب الأقرب أولاً بدقة حتى تحت الفلترة.
relaxed_orderتوسيع تكراري مع ترتيب تقريبي. أسرع من strict_order.الافتراضي الصحيح لمعظم استعلامات RAG المفلترة: قد تتبادل بعض المراكز بين المرشحين المتقاربين جدًا، ولكن يتم الحفاظ على الاسترجاع (recall).

يتم التحكم في سقف المرشحين بواسطة hnsw.max_scan_tuples (الافتراضي 20000).14 إذا كان الفلتر الخاص بك انتقائيًا للغاية لدرجة أن 20 ألف صف لا تظهر تطابقات كافية، فقم بزيادته — أو، بشكل أكثر فائدة، أضف فهرس B-tree على عمود الفلتر حتى يتمكن المخطط من اختيار استراتيجية مختلفة.

قم بتشغيل نفس استعلام الجار الأقرب، مقيدًا بالمستندات باللغة العربية:

-- بدون المسح التكراري (الافتراضي).
SET hnsw.iterative_scan = 'off';
EXPLAIN ANALYZE
SELECT id, source
FROM documents
WHERE language = 'ar'
ORDER BY embedding_h <=> (SELECT embedding_h FROM documents WHERE id = 12345)
LIMIT 10;

في مجموعة بياناتنا الاصطناعية، 25% من الصفوف هي language = 'ar'، لذا فإن السلوك الافتراضي يعيد عشر نتائج — ولكن في مجموعة بيانات حيث يطابق 1% فقط الفلتر، ستحصل عادةً على 2 أو 3. قم بتشغيل المسح التكراري:

SET hnsw.iterative_scan = 'relaxed_order';
SET hnsw.max_scan_tuples = 20000;
EXPLAIN ANALYZE
SELECT id, source
FROM documents
WHERE language = 'ar'
ORDER BY embedding_h <=> (SELECT embedding_h FROM documents WHERE id = 12345)
LIMIT 10;

يُظهر مخرج EXPLAIN الآن قراءات إضافية للمخزن المؤقت (يتم فحص الفهرس لمسافة أبعد) ويتم إرجاع 10 صفوف باستمرار. التكلفة متغيرة: الفلاتر التي تنجح بسهولة تضيف عبئًا صغيرًا؛ الفلاتر التي تتطلب فحصًا عميقًا تكلف أكثر بكثير. قم بالقياس على استعلامات تمثيلية قبل تبديل الافتراضي العام.

نصيحة التشغيل الصادقة: قم بتمكين relaxed_order على مستوى الجلسة لنقاط النهاية التي تنفذ بحثًا متجهًا مفلترًا، واترك off لنقاط النهاية "المزيد من هذا القبيل" غير المفلترة، واستخدم strict_order فقط عندما يكون الكود اللاحق حساسًا للترتيب الدقيق.

الخطوة 7: صيانة HNSW في بيئة التشغيل

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

REINDEX INDEX CONCURRENTLY documents_embedding_h_hnsw_idx;
VACUUM (ANALYZE) documents;

يقوم REINDEX INDEX CONCURRENTLY ببناء فهرس جديد بجانب القديم ويقوم بتبديلهما ذريًا، بحيث لا تتأثر الاستعلامات المتزامنة. يحتاج تقريبًا إلى نفس maintenance_work_mem مثل البناء الأصلي؛ إذا قمت بضبط هذا GUC للبناء الأولي ثم تراجعت عنه، فقم بزيادته مرة أخرى قبل إعادة الفهرسة.

قم بجدولة إعادة الفهرسة خلال نافذة حركة مرور منخفضة معروفة. التكرار الأسبوعي هو وتيرة شائعة؛ قم بتحديد معدله حسب حجم الفهرس حتى لا يتم إعادة بناء جميع الجداول التي يبلغ حجمها 50 جيجابايت في نفس يوم السبت. ادمجه مع VACUUM (ANALYZE) حتى تظل تقديرات الصفوف للمخطط دقيقة — قام pgvector 0.8.0 تحديدًا بتحسين كيفية اختيار المخطط بين فهارس HNSW و B-tree أثناء الاستعلامات المفلترة، وستؤدي قيم pg_class.reltuples القديمة إلى تخريب ذلك.13

التحقق

إذا اتبعت كل خطوة، فيجب أن تنجح الاستعلامات التالية جميعها وتنتج المخرجات الموضحة.

-- 1. إصدار الإضافة هو 0.8.2 (تم إصلاح CVE-2026-3172).
SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- المتوقع: 0.8.2

-- 2. كلا الفهرسين هما HNSW مع فئة العمليات الصحيحة.
SELECT
  indexname,
  indexdef
FROM pg_indexes
WHERE tablename = 'documents';
-- المتوقع: documents_embedding_hnsw_idx يستخدم vector_cosine_ops؛
--           documents_embedding_h_hnsw_idx يستخدم halfvec_cosine_ops.

-- 3. فهرس halfvec يبلغ نصف حجم فهرس vector تقريبًا.
SELECT
  indexname,
  pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_indexes
JOIN pg_class c ON c.relname = indexname
JOIN pg_index ON pg_index.indexrelid = c.oid
WHERE tablename = 'documents';

-- 4. يختار المخطط فهرس HNSW لـ ORDER BY <=> غير مفلتر.
EXPLAIN
SELECT id FROM documents
ORDER BY embedding_h <=> (SELECT embedding_h FROM documents WHERE id = 1)
LIMIT 10;
-- المتوقع: عقدة "Index Scan using documents_embedding_h_hnsw_idx".

-- 5. المسح التكراري متوقف افتراضيًا في جلسة جديدة.
SHOW hnsw.iterative_scan;
-- المتوقع: off

إذا أنتج أي من هؤلاء مخرجات مختلفة، فإن الأسباب الأكثر احتمالاً هي: صورة Docker قديمة (أعد سحب pgvector/pgvector:0.8.2-pg18)، أو maintenance_work_mem كان صغيرًا جدًا (الفهرس لا يزال موجودًا ولكنه دون المستوى الأمثل — قم بـ REINDEX له)، أو مشغل استعلام لا يطابق فئة عمليات الفهرس (سيقول EXPLAIN Seq Scan).

الأخطاء الشائعة

could not resize shared memory segment أثناء CREATE INDEX CONCURRENTLY. السبب: /dev/shm الخاص بـ Docker أصغر من maintenance_work_mem. الإصلاح: ارفع shm_size في docker-compose.yml لمطابقة GUC على الأقل، ثم أعد تشغيل الحاوية.

ERROR: column cannot have more than 2000 dimensions for hnsw index عند تشغيل CREATE INDEX. السبب: يضع pgvector حدًا أقصى لـ HNSW (و IVFFlat) على نوع vector عند 2000 بُعد لأن كل صف في الفهرس يجب أن يتناسب داخل صفحة PostgreSQL التي تبلغ 8 كيلوبايت. الإصلاح: قم بتبديل العمود إلى halfvec، مما يضاعف الحد إلى 4000 بُعد بنصف البايت لكل بُعد؛ بالنسبة للنماذج النادرة التي تخرج أكثر من 4000 بُعد، فإن الفهرسة على نوع bit (التكميم الثنائي) تمد الحد إلى 64,000.

الاستعلامات تقوم بـ EXPLAIN ANALYZE Seq Scan على الرغم من وجود الفهرس. السبب: مشغل الاستعلام لا يطابق فئة عمليات الفهرس. يتطلب فهرس vector_cosine_ops المشغل <=> في ORDER BY. الإصلاح: قم بتغيير إما المشغل في الاستعلام أو فئة العمليات في الفهرس — لا تفترض أبدًا أن المخطط سيختار "ما هو قريب بما يكفي".

الاستعلامات المفلترة تعيد صفوفًا أقل من LIMIT. السبب: القيمة الافتراضية hnsw.iterative_scan = off بالإضافة إلى فلتر انتقائي. الإصلاح: SET hnsw.iterative_scan = 'relaxed_order' على مستوى الجلسة (أو ALTER SYSTEM SET hnsw.iterative_scan = 'relaxed_order'; SELECT pg_reload_conf(); على مستوى الكتلة بالكامل — ترفض GUCs الخاصة بـ HNSW في pgvector صيغة ALTER ROLE … SET على مستوى الدور8)، وارفع hnsw.max_scan_tuples إذا كنت لا تزال ترى نتائج مقتطعة.

extversion تظهر 0.8.1 أو أقدم. السبب: قاعدة بيانات Postgres مستضافة (RDS، Aurora، Cloud SQL، Supabase) لم تقم بترقية 0.8.2 بعد، أو صورة محلية قديمة. الحل على المستضاف: تحقق من قائمة "الإضافات المتاحة" لدى المزود؛ معظمهم طرحوا 0.8.2 في غضون أسابيع من إصدار 2026-02-26. الحل محلياً: Docker compose pull && Docker compose up -d.

بناء HNSW يتوقف لساعات على بضعة ملايين من الصفوف. السبب: maintenance_work_mem صغير جداً بحيث لا يمكنه استيعاب الرسم البياني الجاري إنشاؤه، لذا يتم ترحيل البناء إلى القرص (paging). الحل: ارفع maintenance_work_mem إلى ما لا يقل عن ضعف حجم الفهرس النهائي الذي قمت بقياسه عند 100 ألف صف (بالاستقراء)؛ إذا لم تتمكن من توفير المساحة، فارجع إلى بناء الفهرس خارج المسار النشط على نسخة منطقية متماثلة (logical replica) وترقيتها عبر pg_createsubscriber.15

متى تختار شيئاً آخر

تعد pgvector مع HNSW الخيار الافتراضي الصحيح لأي عبء عمل حيث (أ) بياناتك موجودة بالفعل في Postgres أو تريد نظاماً تشغيلياً واحداً بدلاً من اثنين، (ب) عدد المتجهات لديك عند أو أقل من بضع عشرات الملايين، و (ج) الاستدعاء (recall) هو المقياس الذي يهتم به منتجك. للحصول على مقارنة مدروسة لنماذج التضمين (embedding models) التي تغذي هذا المسار، راجع منشورنا مقارنة نماذج التضمين من word2vec إلى المحولات الحديثة.

إذا كان لديك مئات الملايين من المتجهات مع أهداف زمن انتقال p99 صارمة في نطاق الميلي ثانية الواحدة، فإن محرك ANN مخصص (Qdrant، Milvus، Vespa) سيعطيك منحنى سرعة واستدعاء أفضل على حساب تشغيل قاعدة بيانات ثانية. إذا كان لديك أقل من 10 ملايين متجه وتفضل البساطة على آخر 10% من الأداء، فإن IVFFlat في pgvector أسرع في البناء ويستهلك ذاكرة أقل، على حساب الحاجة لمزيد من الضبط للحفاظ على الاستدعاء — اختر HNSW ما لم يكن لديك سبب لعدم القيام بذلك.

الخطوات التالية ومزيد من القراءة

الحواشي

  1. صفحة منتج OpenAI text-embedding-3-small — مخرجات بـ 1536 بعداً، 0.02 دولار لكل مليون توكن إدخال (قياسي) / 0.01 دولار (دفعة واحدة)، اعتباراً من مايو 2026. https://openai.com/index/new-embedding-models-and-API-updates/ 2

  2. إصدار PostgreSQL 18.3 خارج الدورة، 26 فبراير 2026 — عالج التراجعات التي ظهرت في تحديث 12 فبراير 2026 لـ PostgreSQL 18.2 وما قبله. https://www.postgresql.org/about/news/out-of-cycle-release-scheduled-for-february-26-2026-3241/

  3. إعلان إصدار pgvector 0.8.2 (2026-02-26) — يصلح CVE-2026-3172، وهو تجاوز سعة المخزن المؤقت (buffer overflow) بتقييم CVSS 8.1 مع عمليات بناء فهرس HNSW المتوازية؛ يؤثر على pgvector من 0.6.0 إلى 0.8.1. https://www.postgresql.org/about/news/pgvector-082-released-3245/ ; مدخل NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-3172 2

  4. pgvector Docker Hub — pgvector/pgvector:0.8.2-pg18 (متعدد البنيات، آخر تحديث في فبراير 2026). https://hub.Docker.com/r/pgvector/pgvector/tags

  5. pgvector README — المرجع الأساسي لـ m (الافتراضي 16)، ef_construction (الافتراضي 64)، hnsw.ef_search (الافتراضي 40، الحد الأقصى 1000)، العوامل المدعومة (<=>، <->، <#>، <+>)، وفئات العمليات (vector_cosine_ops، vector_l2_ops، vector_ip_ops، vector_l1_ops، بالإضافة إلى ما يماثلها في halfvec_* و bit_*). https://GitHub.com/pgvector/pgvector/blob/master/README.md 2 3

  6. Neon — "pgvector: بناء فهرس أسرع بـ 30 مرة لتضمينات المتجهات الخاصة بك" — اختبار أداء لبناء HNSW المتوازي عند مليون متجه × 1536 بعداً مع max_parallel_maintenance_workers=8. https://neon.com/blog/pgvector-30x-faster-index-build-for-your-vector-embeddings

  7. Jonathan Katz — "الكمية القياسية والثنائية لـ pgvector" — يشير إلى حوالي 8 جيجابايت لفهرس HNSW بحجم 1 مليون × 1536 بعداً مبني على vector، وينخفض إلى حوالي 3 جيجابايت مع halfvec. https://jkatz05.com/post/postgres/pgvector-scalar-binary-quantization/ 2

  8. مشكلة pgvector رقم 726، "خيارات لتحديد hnsw.ef_search" — توضح أن ALTER ROLE … SET hnsw.ef_search و ALTER DATABASE … SET hnsw.ef_search يتم رفضهما مع رسالة "تم رفض الإذن لتعيين المعلمة"، وأن ALTER SYSTEM SET بالإضافة إلى pg_reload_conf() هو المسار المدعوم على مستوى المجموعة (cluster-wide). https://GitHub.com/pgvector/pgvector/issues/726 2

  • Supabase — "ما الجديد في pgvector 0.7.0" — halfvec يقلل مساحة تخزين المتجهات والفهارس إلى النصف مع تغيير طفيف لا يذكر في الاسترجاع (recall) عند أبعاد التضمين النموذجية. https://Supabase.com/blog/pgvector-0-7-0

  • مدونة قواعد بيانات AWS — "تحميل تضمينات المتجهات بسرعة تصل إلى 67 ضعفًا باستخدام pgvector و Amazon Aurora" — تسريع بناء HNSW بمقدار 67 ضعفًا مع التكميم الثنائي (binary quantization) على Aurora مقارنة بـ pgvector 0.5.1، ورقم الـ 50% لتوفير المساحة لـ halfvec. https://aws.amazon.com/blogs/database/load-vector-embeddings-up-to-67x-faster-with-pgvector-and-amazon-aurora/

  • إعلان إصدار pgvector 0.7.0 — أضاف halfvec، و sparsevec، ومساعدات التكميم العددي/الثنائي، والفهرسة على نوع bit حتى 64,000 بُعد. https://www.postgresql.org/about/news/pgvector-070-released-2852/

  • ملاحظات إصدار pgvector 0.8.0 — تصف فشل الاسترجاع مع التصفية (filter-recall failure) قبل الإصدار 0.8 حيث أن ef_search=40 مع مرشح انتقائي بنسبة 10% يؤدي عادةً إلى بقاء حوالي 4 صفوف فقط. https://www.postgresql.org/about/news/pgvector-080-released-2952/

  • pgvector 0.8.0 — أضاف عمليات فحص الفهرس التكرارية (hnsw.iterative_scan / ivfflat.iterative_scan)، وحسّن تقدير التكلفة لـ HNSW مقابل تخطيط B-tree تحت المرشحات، وأسقط دعم PostgreSQL 12 (الحد الأدنى الآن هو PostgreSQL 13). https://www.postgresql.org/about/news/pgvector-080-released-2952/ 2

  • hnsw.max_scan_tuples — قدم pgvector 0.8 متغير التكوين (GUC) هذا لتقييد ميزانية الفحص التكراري؛ القيمة الافتراضية 20000. https://docs.pgedge.com/pgvector/v0-8-0/iterative-index-scans/

  • PostgreSQL 18 pg_createsubscriber — يقوم بتهيئة نسخة منطقية متماثلة من نسخة فيزيائية، وهو مفيد للترقيات بدون توقف (zero-downtime) ولتخفيف عبء عمليات بناء الفهارس المكلفة. https://www.postgresql.org/docs/18/app-pgcreatesubscriber.html


  • نشرة أسبوعية مجانية

    ابقَ على مسار النيرد

    بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

    بدون إزعاج. إلغاء الاشتراك في أي وقت.