٣١ يوليو ٢٠٢٥

البحث بالنص الكامل (Full-Text Search) في PostgreSQL

يُعَد البحث بالنص الكامل (Full-Text Search أو اختصارًا FTS) تقنية بحث متقدمة تهدف إلى تحسين جودة نتائج الاستعلامات، مما ينعكس إيجابًا على تجربة المستخدم (UX).

في هذا المقال، سأشرح باستخدام الأمثلة كيفية تطبيق البحث بالنص الكامل في اللغة العربية، التي أعتبرها من أكثر اللغات تحديًا لهذه التقنية نظرًا لتعقيداتها الكبيرة في عملية التسوية (Normalization).

مطابقة الأنماط باستخدام LIKE/ILIKE

لا تُعتبر مطابقة الأنماط بحثًا بالنص الكامل (FTS)، ولكنها قد تكون كافية للمشاريع البسيطة التي لا يمثل فيها البحث ميزة حيوية.

الفرق بين LIKE و ILIKE يكمن في حساسية حالة الأحرف (case sensitivity)؛ حيث إن LIKE حساسة لحالة الأحرف، بينما ILIKE ليست كذلك. فمثلًا، البحث باستخدام LIKE 'Apple%' سيطابق Apple أو Apples فقط، بينما ILIKE 'Apple%' سيطابق كل تنويعات حالة الأحرف مثل apples و APPLE وغيرها.

لكن اللغة العربية لا تحتوي على حالة أحرف مختلفة (cases)، لذا لا يوجد أي تأثير لهذا الأمر.

مع ذلك، قد يصبح هذا النوع من البحث بطيئًا جدًا عند التعامل مع مجموعات بيانات ضخمة.

الـ trigram هو مجموعة من ثلاثة أحرف متتالية مأخوذة من سلسلة نصية. يمكننا قياس التشابه بين سلسلتين نصيتين عن طريق حساب عدد الـ trigrams المشتركة بينهما. هذه الفكرة البسيطة أثبتت فعاليتها الكبيرة في قياس تشابه الكلمات في العديد من اللغات الطبيعية - (postgresql.org).

يمكن تفعيل هذه الميزة بسهولة عبر تنفيذ أمر الـ SQL التالي:

CREATE EXTENSION pg_trgm; -- تفعيل وحدة البحث باستخدام trigram

يمكنك قراءة المزيد عن العوامل (operators) التي توفرها pg_trgm في الموقع الرسمي.

يُعد البحث باستخدام trigram تقنية جيدة للبحث التقريبي (fuzzy search) والتعامل مع الأخطاء الإملائية للمستخدمين. على سبيل المثال:

SELECT similarity('book', 'bok'); -- returns 0.5
 
ملاحظة

عتبة التشابه الافتراضية هي 0.3، ويمكنك تعديلها.

SELECT similarity('مكتبة', 'مكتبي'); -- returns 0.5

ولكن بالنسبة للنصوص العربية، نظرًا لكونها لغة معقدة، قد لا تعطي pg_trgm أحيانًا نتائج جيدة. على سبيل المثال:

SELECT similarity('كتب', 'كتاب'); -- returns 0.2857143

هذه النتيجة تعتبر غير متطابقة.

أيضًا، عندما تحتوي الكلمة على علامات التشكيل، يتم التعامل معها كأحرف مختلفة تمامًا:

SELECT similarity('تشكيل', 'تَشْكِيلٍ'); -- returns 0.06666667

لذا، يجب توخي الحذر ومحاولة تسوية النص، على الأقل عن طريق إزالة التشكيل.

لن أخوض في تفاصيل عوامل trigram الأخرى التي توفرها pg_trgm، ربما أكتب مقالًا آخر لاستكشاف البحث باستخدام trigram بتعمق أكبر.

رغم ذلك، يعتبر هذا حلًا جيدًا عند التعامل مع عدد قليل من الكلمات، ولكنه قد يكون بطيئًا جدًا مع النصوص الكبيرة.

الفهرسة (Indexing)

يوفر البحث بـ trigram أيضًا فهارس من نوع GIN و GIST لجعل استعلاماتك أسرع بكثير.

-- GIN index
CREATE INDEX trgm_idx ON table_name USING GIN (col_name gin_trgm_ops);
-- GIST index
CREATE INDEX trgm_idx ON table_name USING GIST (col_name gist_trgm_ops);

مثال أخير على بحث في متجر كتب:

SELECT * FROM books WHERE title % 'صحيح ملم';

من المفترض أن يُرجع هذا الاستعلام الكتاب "صحيح مسلم". إذا قمت بتشغيل نفس الاستعلام باستخدام LIKE/ILIKE، فلن تحصل على أي نتيجة، وهذه هي الميزة الجيدة في البحث باستخدام trigram: التقاط الأخطاء الإملائية.

البحث بالنص الكامل (FTS): الأساسيات

قبل أن نفهم البحث بالنص الكامل (FTS)، يجب أن نتساءل: لماذا نحتاجه؟

تظهر قوة البحث بالنص الكامل الحقيقية عندما نريد تطبيق ميزة "البحث بالمعنى". على سبيل المثال، البحث عن كلمة "سيارة" يجب أن يُرجع أيضًا النتائج التي تذكر كلمة "عربة".

يعتمد البحث بالنص الكامل في PostgreSQL على عامل المطابقة @@، الذي يُرجع true إذا تطابق tsvector مع tsquery.

مثال أساسي على عوامل FTS:

fts=# SELECT to_tsvector('arabic', 'كيف يسد الخيال فجوات التاريخ؟');
                                 to_tsvector
---------------------------------------------------------
 'تاريخ':5 'جوا':4 'خيال':3 'كيف':1 'يسد':2

تقوم دالة to_tsvector بتحويل النص إلى نوع tsvector. الـ tsvector هو قائمة مُحسَّنة ومُرتبة من الجذوع اللغوية (lexemes) المميزة، مع مواقعها في المستند الأصلي.

 
ملاحظة

التسوية (Normalization/Stemming/Lemmatization): هي عملية اختزال الكلمات إلى صيغتها الأساسية أو جذرها، والتي تسمى lexeme. على سبيل المثال، الكلمات "يجري"، "يجرون" يتم اختزالها جميعًا إلى الجذر "جري". هذا يسمح للبحث عن "جري" بمطابقة جميع تنويعاتها.

ولكن إذا لاحظت، فقد تم تحويل "فجوات" إلى "جوا"، وهذا خطأ فادح، وسنتطرق إلى هذه المشكلة لاحقًا، حيث إن دعم اللغة العربية في FTS لا يزال ضعيفًا جدًا.

مثال أبسط يوضح مدى ضعف الدعم:

fts=# SELECT to_tsvector('arabic', 'يجري'), to_tsvector('arabic', 'يجرون');
-[ RECORD 1 ]----------
to_tsvector | 'يجر':1
to_tsvector | 'يجرون':1

أما دالة to_tsquery، فستُرجع نفس الكلمات بعد تسويتها كما في tsvector لضمان اتساق النص عند إجراء المطابقة:

fts=# SELECT plainto_tsquery('arabic', 'كيف يسد الخيال فجوات التاريخ؟');
                               plainto_tsquery
-------------------------------------------------------
 'كيف' & 'يسد' & 'خيال' & 'جوا' & 'تاريخ'

كما ترى، لا تزال تحول "فجوات" إلى "جوا".

مثال على استخدام كليهما:

fts=# SELECT to_tsvector('arabic', 'كيف يسد الخيال فجوات التاريخ؟') @@ to_tsquery('arabic', 'تاريخ');
 ?column?
----------
 t

الحرف t يرمز إلى true، مما يعني أن المستند والاستعلام قد تطابقا.

تُعد عملية التسوية (Normalization) هي الجزء الأكثر تحديًا في FTS بالنسبة للغة العربية، حيث تحتاج إلى قواميس جيدة إذا كنت ترغب في الحصول على نتائج ممتازة.

البحث بالنص الكامل (FTS): القواميس (Dictionaries)

تُستخدم القواميس لإزالة الكلمات التي لا ينبغي أخذها في الاعتبار أثناء البحث (stop words)، ولتسوية الكلمات بحيث تتطابق الأشكال المشتقة المختلفة من نفس الكلمة. الكلمة التي يتم تسويتها بنجاح تسمى lexeme. - (postgresql.org)

هناك 6 أنواع رئيسية من القواميس في PostgreSQL يمكننا استخدامها لتحويل الكلمات إلى جذوعها اللغوية (lexmize):

  1. قاموس كلمات التوقف (Stop Words Dictionary): يزيل الكلمات الشائعة (stop words) في اللغة المحددة (مثل: the, of, عن, على, فـ...).
  2. القاموس البسيط (Simple Dictionary): يعمل هذا القالب عن طريق تحويل الكلمة إلى أحرف صغيرة والتحقق منها مقابل ملف يحتوي على كلمات التوقف. هو أبسط قاموس يمكنك البدء به.
  3. قاموس المترادفات (Synonym Dictionary): يستخدم لاستبدال كلمة بمرادفها. على سبيل المثال، نريد أن يتم تحويل البحث عن "عربة"، "عربية"، "كرهبة" إلى lexeme واحد وهو "سيارة".
  4. قاموس الكنوز (Thesaurus Dictionary): يشبه قاموس المترادفات، ولكنه يدعم العبارات. على سبيل المثال، نريد أن تصبح "المدينة المنورة" "المدينة".
  5. قاموس Ispell Dictionary: يدعم هذا القالب القواميس الصرفية (morphological)، التي يمكنها تسوية العديد من الأشكال اللغوية المختلفة للكلمة إلى نفس الـ lexeme، مثل تحويل "أكلنا"، "أكلتم"، "يأكلون" إلى "أكل".
  6. قاموس Snowball Dictionary: هو مجزّئ جذور (stemmer) بسيط جدًا. يقوم بربط الأشكال المختلفة لنفس الكلمة بجذر مشترك (مثل: connections, connective, connected, and connecting إلى connect).
 
هام

الإعداد الافتراضي to_tsvector('arabic', ...) يستخدم فعليًا قاموس Snowball لتحويل الكلمات إلى جذوعها، وكما رأيت أعلاه، فقد حوّل "فجوات" إلى "جوا"، وهذا خطأ. يمكنك تجربة تجزئة الجذور باستخدام Snowball بلغات مختلفة في موقعهم الرسمي.

البحث بالنص الكامل (FTS): التطبيق العملي

لتطبيق البحث بالنص الكامل باللغة العربية، سنستخدم ملفات بصيغة hunspell، حيث لا توجد ملفات ispell للغة العربية.

يمكنك تحميلها من هنا.

كل ما تحتاجه هو ملفي ar.dic و ar.aff.

أنا أستخدم PostgreSQL داخل حاوية docker، فإذا أردت المتابعة معي، يمكنك تشغيل الأوامر التالية:

# أنا قمت بتغيير أسماء الملفات لتطابق ما يتوقعه Ispell
docker cp ./ar.aff ftsdb:/usr/share/postgresql/16/tsearch_data/ar.affix
docker cp ./ar.dic ftsdb:/usr/share/postgresql/16/tsearch_data/ar.dict

ftsdb هو اسم الحاوية الخاصة بي، لذا فأنت تقوم بنسخ الملفات من جهازك إلى داخل الحاوية.

بعد ذلك، قمنا بنسخ هذه الملفات إلى المسار /usr/share/postgresql/16/tsearch_data. أنا أستخدم الإصدار 16 من PostgreSQL، لذا تأكد من تشغيل pg_config --sharedir لمعرفة المسار الصحيح لديك إذا لم تكن متأكدًا.

إذا حاولت استعراض محتويات المسار usr/share/postgresql/16/tsearch_data باستخدام ls، ستجد ملفات وقوالب للغات مختلفة، يمكنك في الواقع نسخ بعضها لبناء قواميسك المخصصة.

مثال على قالب ملف قاموس المترادفات:

root@efc3952f8152:/# cat usr/share/postgresql/16/tsearch_data/synonym_sample.syn
postgres	pgsql
postgresql	pgsql
postgre	pgsql
gogle	googl
indices	index*

كما ذكرت سابقًا، هنا البحث عن "postgres"، "postgresql"، أو "postgre" سيتم تحويله إلى lexeme واحد هو "pgsql"، أي أنها نفس الكلمة.

لنعد إلى موضوعنا.

الآن بعد نسخ الملفات، سنقوم بإنشاء قاموس مخصص لاستخدام هذه الملفات:

CREATE TEXT SEARCH DICTIONARY arabic_hunspell (
   TEMPLATE  = ispell,
   DictFile  = ar,
   AffFile   = ar,
   -- StopWords = ar
);
 
CREATE TEXT SEARCH CONFIGURATION public.arabic (
    COPY = pg_catalog.english
);
 
ALTER TEXT SEARCH CONFIGURATION arabic
    ALTER MAPPING
    FOR
        asciiword, asciihword, hword_asciipart, word, hword, hword_part
    WITH
        arabic_hunspell;
 
ملاحظة

لقد قمت بتعطيل StopWords لأننا لا نملك ملفًا مخصصًا لها، ولكن كتمرين، يمكنك نسخ قالب من مجلد tsearch_data ووضع كلمات التوقف العربية فيه، ثم إعادة تكوين القاموس لاستخدام ملفك المخصص.

ما فعلناه أعلاه هو إنشاء قاموس ispell مخصص، ثم إنشاء إعدادات بحث أساسية منسوخة من إعدادات اللغة الإنجليزية، وأخيرًا قمنا بتعديل هذه الإعدادات لتعمل مع قاموسنا المخصص.

الآن إذا أردنا اختبار الإعدادات الجديدة التي تستخدم الملفات المخصصة، يمكننا تشغيل:

fts=# SELECT ts_debug('arabic', 'فجوات');
-[ RECORD 1 ]-----------------------------------------------------------------------------------------------------------------------------
ts_debug | (word,"Word, all letters",فجوات,"{arabic_hunspell}",arabic_hunspell,"{جو,فجوة,جوة,جو,فجوة,جوة,جو,فجوة,جوة,جو,فجوة,جوة}")

كما ترى، فإنه يستخدم قاموسنا arabic_hunspell والناتج مختلف لكلمة "فجوات".

هل هذا جيد؟ لا، لا يزال غير مثالي، كما سنرى أدناه:

fts=# select to_tsquery('arabic', 'كتب');
-[ RECORD 1 ]---------------------------------------------------------
to_tsquery | 'كتب' | 'تب' | 'كتب' | 'تب' | 'تبي' | 'تب' | 'كتب' | 'تب'
 
fts=# select to_tsvector('arabic', 'يكتبون');
-[ RECORD 1 ]-----------
to_tsvector | 'يكتبون':1

كلمة "يكتبون" لم يتم تحويلها إلى جذرها بشكل صحيح، وبالتالي قد لا يعطي البحث نتائج دقيقة.

لنجرب الآن تكوين قاموس المترادفات ونرى كيف يعمل. لقد أنشأت ملفًا باسم ar.syn بالمحتوى التالي:

يثرب المدينة
طيبة المدينة

هذا يخبر PostgreSQL بتحويل كلتا الكلمتين إلى "المدينة".

بعد نسخ الملف إلى مجلد tsearch كما فعلنا من قبل مع قاموس hunspell، نقوم بإنشاء قاموس المترادفات لاستخدام هذا الملف، وتغيير الإعدادات لاستخدامه:

CREATE TEXT SEARCH DICTIONARY arabic_syn (
    TEMPLATE = synonym,
    SYNONYMS = ar
);
 
ALTER TEXT SEARCH CONFIGURATION arabic
    ALTER MAPPING FOR
        asciiword, asciihword, hword_asciipart,
        word, hword, hword_part
    WITH arabic_syn, arabic_hunspell;

الآن أصبح PostgreSQL يحول الكلمات إلى مرادفاتها التي حددناها في الملف. وهذا مثال على نتائج الاستعلام أثناء العمل:

البحث بالنص الكامل (FTS): الأداء والتحسين (Performance & Optimization)

قد يصبح البحث بالنص الكامل بطيئًا جدًا عندما تصبح بياناتك ضخمة جدًا، وبالتالي هناك شيئان يجب عليك القيام بهما:

  1. الفهرسة (Index): لكل لغة تحتاج إلى فهرس خاص بها في حال كنت تدعم لغات متعددة.
    CREATE INDEX arabic_col_idx ON table_name USING GIN (to_tsvector('arabic', col_name));

    يمكنك استخدام فهرس GIST أيضًا.

  2. حفظ الـ vector في قاعدة البيانات: لتوفير وقت تحويل المحتوى إلى lexeme في كل مرة.
    CREATE TABLE my_table (
      id serial PRIMARY KEY,
      content text,
      content_vector tsvector GENERATED ALWAYS AS (to_tsvector('arabic', content)) STORED
    );

    الآن يمكنك استخدام هذا العمود مباشرة عند البحث:

    SELECT * FROM my_table WHERE content_vector @@ to_tsquery('arabic', 'طيبة');

البحث بالنص الكامل (FTS): ترتيب النتائج (Ranking)

تحاول عملية الترتيب قياس مدى صلة المستندات باستعلام معين، بحيث عند وجود العديد من التطابقات، يمكن عرض الأكثر صلة أولاً. - (postgresql.org)

هناك دالتان للترتيب في PostgreSQL:

  • ts_rank(): ترتب النتائج بناءً على تكرار الـ lexemes المتطابقة.
  • ts_rank_cd(): تحسب ترتيب "كثافة التغطية" (cover density).
SELECT
   name,
   ts_rank_cd(to_tsvector('arabic', name), to_tsquery('arabic', 'text')) as rank
FROM documents
WHERE to_tsvector('arabic', name) @@ to_tsquery('arabic', 'text')
ORDER BY rank DESC;

مراجعة نهائية

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

مقالات مرتبطة

ابق على اطلاع على أحدث الأفكار والتقنيات من خلال زيارة مدونتنا

البوتات تزداد ذكاءً — وكذلك دفاعاتنا

البوتات تزداد ذكاءً — وكذلك دفاعاتنا

البوتات تزداد ذكاءً — وكذلك دفاعاتنا…

١٧ يوليو ٢٠٢٥
الاستخلاص المتقدم لبيانات الويب من أجل الذكاء الاصطناعي والتعلم الآلي: التقنيات، والأمان، والأخلاقيات

الاستخلاص المتقدم لبيانات الويب من أجل الذكاء الاصطناعي والتعلم الآلي: التقنيات، والأمان، والأخلاقيات

تعلّم استخراج بيانات الويب للذكاء الاصطناعي والتعلم الآلي، باستخدام تقنيات مثل اكتشاف واجهات برمجة ا…

٨ مايو ٢٠٢٥
مستقبل الحوسبة السحابية

مستقبل الحوسبة السحابية

في عالم اليوم الرقمي السريع الخطى، أصبحت الحوسبة السحابية ركيزة أساسية للابتكار والكفاءة في الأعمال.…

٧ يناير ٢٠٢٤