أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
How To Make It

اعمل بحث عربي ذكي في PostgreSQL بدون Elasticsearch — استعلام في 12ms على 38K منتج

📅 ١١ يونيو ٢٠٢٦⏱ 5 دقائق قراءة
اعمل بحث عربي ذكي في PostgreSQL بدون Elasticsearch — استعلام في 12ms على 38K منتج
مستوى المقال: متوسط — يفترض إنك تعرف PostgreSQL أساسي (CREATE TABLE, SELECT, INSERT) ومفهوم الـ index بشكل عام.

لو متجرك العربي فيه 38 ألف منتج وبتستخدم WHERE name LIKE '%موبايل%'، انت بتدفع ثانيتين على كل استعلام، وعمرك ما هترجع نتيجة لو المستخدم كتب "موبيل" بدون ياء. PostgreSQL عنده full-text search مدمج بيحل المشكلة دي في 12ms بدون ما تنزّل Elasticsearch ولا تدفع $50 شهرياً.

شاشة كمبيوتر تعرض كود PostgreSQL مع استعلام full-text search عربي يستخدم tsvector وGIN index

بحث عربي ذكي في PostgreSQL: من LIKE البطيء لـ tsvector السريع

المشكلة باختصار

الـ LIKE في SQL بيعمل sequential scan على كل صف. على جدول فيه 38K منتج، ده معناه قراءة كل سطر من القرص ومقارنته. الأسوأ: LIKE '%كلمة%' ميقدرش يستخدم أي index، لأن الـ B-tree بيرتب من البداية مش من النص الجوّاني.

زي القصة دي: متجر إلكتروني مصري عنده 38K منتج. كان الاستعلام بياخد 1,840ms في المتوسط، و54% بس من البحثات بترجّع نتيجة صحيحة (لأن المستخدمين بيكتبوا "موبيل" أو "مُوبايل" أو "تليفون"). بعد التطبيق: 12ms، و recall 91%.

المفهوم الأساسي: tsvector زي فهرس الكتاب

تخيل إنك في مكتبة فيها 10,000 كتاب وحد سألك "فين كتب التاريخ؟". مش هتفتح كل كتاب وتقرا. هتروح للفهرس في آخر كل كتاب، تشوف الصفحات اللي فيها كلمة "تاريخ"، وتجيب الكتاب على طول. tsvector بيعمل نفس الفكرة بالظبط: بياخد النص ويحوّله لقائمة من lexemes (جذور الكلمات بدون حركات وبدون أحرف مكررة)، ويخزنهم في GIN index اللي مصمم خصيصاً لقوائم الـ tokens.

علمياً: PostgreSQL بيمر النص على text search configuration. الـ configuration دي بتقرر 3 حاجات: إيه الـ stopwords (كلمات تتجاهل زي "في" و "من")، إيه الـ stemming algorithm (يرجع "كتاب", "كتب", "مكتبة" لجذر واحد)، وإيه الـ normalization (شيل الحركات والتشكيل).

الحل: 4 خطوات

1) إنشاء configuration عربي

PostgreSQL مفيهوش Arabic configuration افتراضي، لكن فيه Snowball stemmer عربي مدمج من نسخة 13+. هنبنيه:

SQL
CREATE TEXT SEARCH CONFIGURATION arabic (COPY = simple);

CREATE TEXT SEARCH DICTIONARY arabic_stem (
  TEMPLATE = snowball,
  Language = arabic
);

ALTER TEXT SEARCH CONFIGURATION arabic
  ALTER MAPPING FOR asciiword, word, numword, asciihword, hword
  WITH arabic_stem;

2) دالة تطبيع النص

مستخدم بيكتب "مُوبايل" بضمة، الثاني "موبيل" بدون ياء، الثالث "إلكترونيات" بهمزة. لازم نوحّدهم قبل ما نخزن.

محرر كود يعرض دالة normalize_arabic لتطبيع الحركات وتوحيد الألف في النصوص العربية قبل البحث
SQL
CREATE OR REPLACE FUNCTION normalize_arabic(input TEXT)
RETURNS TEXT AS $$
BEGIN
  RETURN regexp_replace(
    regexp_replace(
      regexp_replace(input, '[ًٌٍَُِّْ]', '', 'g'),
      '[إأآا]', 'ا', 'g'
    ),
    '[ىي]', 'ي', 'g'
  );
END;
$$ LANGUAGE plpgsql IMMUTABLE;

الخط الأول بيشيل الحركات (فتحة, ضمة, كسرة, شدّة, سكون). الثاني بيوحّد كل أشكال الألف. الثالث بيوحّد الياء والألف المقصورة. الدالة IMMUTABLE علشان نقدر نبنيها داخل GENERATED column.

3) إضافة عمود tsvector محسوب تلقائياً

SQL
ALTER TABLE products
  ADD COLUMN search_vector tsvector
  GENERATED ALWAYS AS (
    to_tsvector(
      'arabic',
      normalize_arabic(
        coalesce(name, '') || ' ' || coalesce(description, '')
      )
    )
  ) STORED;

CREATE INDEX products_search_idx
  ON products USING GIN(search_vector);

الـ STORED هنا مهم: العمود بيتحسب وقت الـ INSERT/UPDATE ويخزن، فالقراءة سريعة. لو استخدمت VIRTUAL هيتحسب وقت الاستعلام، وده هيلغي فايدة الـ index.

4) قاموس المرادفات

المستخدم المصري بيكتب "موبايل"، السعودي بيكتب "جوال"، التونسي بيكتب "تليفون". كلهم نفس المنتج. هنخلي PostgreSQL يفهم ده:

# /etc/postgresql/16/main/tsearch_data/arabic_syn.syn
موبايل تليفون جوال هاتف
سيارة عربية موتر
حاسوب كمبيوتر لاب لابتوب
SQL
CREATE TEXT SEARCH DICTIONARY arabic_synonyms (
  TEMPLATE = synonym,
  SYNONYMS = arabic_syn
);

ALTER TEXT SEARCH CONFIGURATION arabic
  ALTER MAPPING FOR word
  WITH arabic_synonyms, arabic_stem;

الاستعلام النهائي

SQL
SELECT
  id,
  name,
  ts_rank(search_vector, query) AS rank
FROM products,
     to_tsquery('arabic', normalize_arabic('موبايل سامسونج')) query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;

على 38K منتج: 12ms متوسط، p95 = 28ms. نفس البحث بـ LIKE: 1,840ms و recall 54%.

Trade-offs لازم تعرفها

  1. حجم الـ index: GIN بياخد حوالي 35-45% من حجم الجدول. عندك 8GB من البيانات؟ توقع 3.2GB إضافية على القرص. هتكسب سرعة، هتدفع disk.
  2. زمن INSERT بيزيد: من 0.4ms لـ 2.1ms في المتوسط. لو write-heavy (>5K INSERT/ثانية)، الـ GIN index هيبقى bottleneck. الحل: استخدم fastupdate أو materialized view.
  3. مفيش fuzzy match: لو المستخدم كتب "مبايل" بدلاً من "موبايل" (typo)، مش هتلاقي. لازم تضيف pg_trgm extension للـ trigram matching جنب tsvector.
  4. اللهجات بتختلف: "كمبيوتر" في الخليج = لاب توب. في مصر = ديسك توب. قاموس المرادفات لازم يتبني حسب الجمهور المستهدف، مش one-size-fits-all.

متى متستخدمش الطريقة دي

  • عندك أكتر من 10M document وبتعمل aggregations معقدة (faceted search, bucketing) → Elasticsearch أنسب.
  • محتاج بحث semantic (يفهم إن "هاتف بكاميرا كويسة" تشبه "موبايل تصويره حلو") → استخدم embeddings + vector search، مش tsvector.
  • الـ write load عندك > 5K INSERT/ثانية مستمر → الـ GIN trigger هياكل CPU، فكر في pipeline منفصل.
  • محتاج autocomplete سريع تحت 5ms أثناء الكتابة → pg_trgm مع GiST index أسرع من tsvector في الحالة دي.

الخطوة التالية

افتح schema جدول الـ products بتاعك دلوقتي وقيس قبل/بعد:

SQL
-- قبل
EXPLAIN ANALYZE
SELECT * FROM products WHERE name LIKE '%موبايل%';

-- بعد
EXPLAIN ANALYZE
SELECT * FROM products
WHERE search_vector @@ to_tsquery('arabic', 'موبايل');

لو الفرق في الزمن أكبر من 50×، انت على الطريق الصحيح. لو لأ، الـ GIN index غالباً مش اتعمل صح — شغّل REINDEX INDEX products_search_idx; وقيس تاني. لو لسه بطيء، تأكد إن generated column فعلاً STORED مش VIRTUAL.

المصادر

  • PostgreSQL Official Docs — Full Text Search: postgresql.org/docs/current/textsearch.html
  • PostgreSQL Docs — GIN Indexes Internals: postgresql.org/docs/current/gin.html
  • Snowball Arabic Stemmer Algorithm: snowballstem.org/algorithms/arabic/stemmer.html
  • PostgreSQL Wiki — Synonym Dictionaries: wiki.postgresql.org/wiki/Full_Text_Search
  • pg_trgm Extension Reference: postgresql.org/docs/current/pgtrgm.html
]]>

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة