المستوى: متوسط. الدليل ده يفترض إنك تعرف أساسيات SQL وعندك جدول PostgreSQL شغّال (الإصدار 13 أو أحدث).
هتخرج من المقال ده وعندك بحث عربي كامل جوّه PostgreSQL يرجّع النتيجة في أقل من 10 مللي ثانية على ملايين الصفوف، من غير ما تركّب Elasticsearch ولا تدفع سيرفر زيادة.
بحث نصي عربي كامل في PostgreSQL بدون Elasticsearch
المشكلة باختصار
أغلب المطورين بيعملوا البحث بـ ILIKE '%كلمة%'. الطريقة دي بتفشل على مقياس حقيقي. مع كل استعلام، PostgreSQL بيقرأ كل صف في الجدول (Sequential Scan). على جدول بـ 2 مليون منتج، ده معناه مئات المللي ثانية لكل بحث، وبيزيد كل ما البيانات تكبر. وكمان ILIKE مبيرتّبش النتائج بالصلة، فالنتيجة الأهم ممكن تطلع في الآخر.
الفكرة أولًا: فهرس آخر الكتاب
تخيّل كتاب 900 صفحة وعايز كل صفحة فيها كلمة "التصميم". قدامك طريقتين. الأولى: تقلّب الكتاب صفحة صفحة من الأول للآخر. دي بطيئة، وهي بالظبط اللي بيعملها ILIKE. الطريقة التانية: تفتح فهرس آخر الكتاب، تلاقي كلمة "التصميم" وجنبها أرقام الصفحات على طول.
ده بالظبط اسمه الفهرس المعكوس (Inverted Index)، وهو أساس أي محرك بحث. بدل ما يدوّر في كل النص، بيبني قايمة مقلوبة: كل كلمة وجنبها الصفوف اللي فيها.
علميًا: PostgreSQL بيحوّل النص لنوع بيانات اسمه tsvector. الـ tsvector بيقطّع الجملة لكلمات (tokens)، بيشيل الكلمات الشائعة (زي "في" و"من")، وبيرجّع الكلمة لأصلها (تجذير). بعد كده فهرس GIN بيربط كل كلمة بالصفوف اللي فيها، فالبحث بيبقى قفزة مباشرة مش مسح كامل.
الخطوات: من عمود نصي لبحث مفهرس
الافتراض إن عندك جدول منتجات بالشكل ده: عمود title وعمود body فيهم نص عربي.
1) ضيف عمود tsvector محسوب تلقائيًا
بدل ما تحدّث الفهرس بإيدك، خلّي PostgreSQL يحسبه لوحده في عمود مخزّن:
ALTER TABLE products
ADD COLUMN search_vec tsvector
GENERATED ALWAYS AS (
to_tsvector('arabic', coalesce(title,'') || ' ' || coalesce(body,''))
) STORED;العمود ده بيتحدّث لوحده مع أي INSERT أو UPDATE، فمفيش داعي لـ trigger يدوي. لو PostgreSQL عندك أقدم من 12، استخدم trigger بدل الـ generated column.