دليل تعليمي لضبط أداء pgvector HNSW في Postgres 18 لبيئة الإنتاج ٢٠٢٦
١٦ مايو ٢٠٢٦
ملخص
إنشاء فهرس 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.
psql16+ على الجهاز المضيف (يأتي العميل مع معظم عمليات تثبيت Postgres وصيغةlibpqفي Homebrew).- حوالي 4 جيجابايت من ذاكرة RAM المتاحة إذا اتبعت قسم البناء المتوازي.
- حوالي 15 دقيقة، وإما مفتاح OpenAI API لنموذج
text-embedding-3-small(المستخدم هنا كمثال قياسي بـ 1536 بُعداً، بتكلفة ~0.02 دولار لكل مليون توكن مدخل1) أو أي مصدر آخر لمتجهات float بـ 1536 بُعداً.
الإصدارات المحددة المستخدمة في الدليل:
| المكون | الإصدار | ملاحظات |
|---|---|---|
| PostgreSQL | 18.3 | إصدار خارج الدورة بتاريخ 26-02-20262 |
| pgvector | 0.8.2 | صدر في 26-02-2026؛ يعالج ثغرة CVE-2026-31723 |
| صورة Docker | pgvector/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:
- المنفذ 5433 يتجنب التعارض مع أي نسخة Postgres محلية لديك بالفعل على المنفذ 5432.
shm_size: 2gbيتطابق معmaintenance_work_mem. تشارك عمليات بناء HNSW المتوازية الذاكرة عبر/dev/shm؛ وبدونshm_sizeمطابق، ستتوقف العمال (workers) بأخطاء OOM غامضة في نهاية عملية بناء قد تستغرق ساعات.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 هي:
| البارامتر | الافتراضي | ما يتحكم فيه |
|---|---|---|
m | 16 | الحد الأقصى لعدد الاتصالات ثنائية الاتجاه لكل طبقة في الرسم البياني (graph). أكبر = استرجاع (recall) أعلى، فهرس أكبر، بناء أبطأ. |
ef_construction | 64 | حجم قائمة المرشحين الديناميكية أثناء البناء. أكبر = رسم بياني بجودة أفضل، بناء أبطأ. |
hnsw.ef_search | 40 | حجم قائمة المرشحين وقت الاستعلام. أكبر = استرجاع أعلى، زمن استجابة (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 ما لم يكن لديك سبب لعدم القيام بذلك.
الخطوات التالية ومزيد من القراءة
- المنشور المصاحب ترقية Postgres 18 بدون وقت توقف باستخدام pg_createsubscriber يغطي الهجرة من Postgres 17 إلى 18 في حال لم تكن تستخدم الإصدار المستهدف بالفعل.
- بالنسبة للمخاوف المتعلقة بمستوى الاتصال بمجرد تشغيل pgvector في الإنتاج، فإن تجميع اتصالات Postgres في الإنتاج باستخدام PgBouncer و Supavisor يستعرض خيارات المجمع (pooler) التي تتفاعل مع استعلامات المتجهات الكثيفة بالبيانات المعدة مسبقاً (prepared-statement).
- إذا كنت بحاجة أيضاً إلى إبطال البيانات في الوقت الفعلي عند تغير التضمينات، فإن Postgres LISTEN/NOTIFY للتواجد في الوقت الفعلي يوضح نمط القناة الذي يكمل البحث المتجهي بشكل جيد.
- ملف pgvector README5 قصير ومكثف ومرجعي — أعد قراءته بعد العمل على هذا البرنامج التعليمي؛ ستفهم كل شيء بشكل مختلف.
الحواشي
-
صفحة منتج OpenAI text-embedding-3-small — مخرجات بـ 1536 بعداً، 0.02 دولار لكل مليون توكن إدخال (قياسي) / 0.01 دولار (دفعة واحدة)، اعتباراً من مايو 2026. https://openai.com/index/new-embedding-models-and-API-updates/ ↩ ↩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/ ↩
-
إعلان إصدار 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
-
pgvector Docker Hub —
pgvector/pgvector:0.8.2-pg18(متعدد البنيات، آخر تحديث في فبراير 2026). https://hub.Docker.com/r/pgvector/pgvector/tags ↩ -
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 -
Neon — "pgvector: بناء فهرس أسرع بـ 30 مرة لتضمينات المتجهات الخاصة بك" — اختبار أداء لبناء HNSW المتوازي عند مليون متجه × 1536 بعداً مع
max_parallel_maintenance_workers=8. https://neon.com/blog/pgvector-30x-faster-index-build-for-your-vector-embeddings ↩ -
Jonathan Katz — "الكمية القياسية والثنائية لـ pgvector" — يشير إلى حوالي 8 جيجابايت لفهرس HNSW بحجم 1 مليون × 1536 بعداً مبني على
vector، وينخفض إلى حوالي 3 جيجابايت معhalfvec. https://jkatz05.com/post/postgres/pgvector-scalar-binary-quantization/ ↩ ↩2 -
مشكلة 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 ↩