٣١ يوليو ٢٠٢٥
يُعَد البحث بالنص الكامل (Full-Text Search أو اختصارًا FTS) تقنية بحث متقدمة تهدف إلى تحسين جودة نتائج الاستعلامات، مما ينعكس إيجابًا على تجربة المستخدم (UX).
في هذا المقال، سأشرح باستخدام الأمثلة كيفية تطبيق البحث بالنص الكامل في اللغة العربية، التي أعتبرها من أكثر اللغات تحديًا لهذه التقنية نظرًا لتعقيداتها الكبيرة في عملية التسوية (Normalization).
لا تُعتبر مطابقة الأنماط بحثًا بالنص الكامل (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 بتعمق أكبر.
رغم ذلك، يعتبر هذا حلًا جيدًا عند التعامل مع عدد قليل من الكلمات، ولكنه قد يكون بطيئًا جدًا مع النصوص الكبيرة.
يوفر البحث بـ 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)، يجب أن نتساءل: لماذا نحتاجه؟
تظهر قوة البحث بالنص الكامل الحقيقية عندما نريد تطبيق ميزة "البحث بالمعنى". على سبيل المثال، البحث عن كلمة "سيارة" يجب أن يُرجع أيضًا النتائج التي تذكر كلمة "عربة".
يعتمد البحث بالنص الكامل في 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 بالنسبة للغة العربية، حيث تحتاج إلى قواميس جيدة إذا كنت ترغب في الحصول على نتائج ممتازة.
تُستخدم القواميس لإزالة الكلمات التي لا ينبغي أخذها في الاعتبار أثناء البحث (stop words)، ولتسوية الكلمات بحيث تتطابق الأشكال المشتقة المختلفة من نفس الكلمة. الكلمة التي يتم تسويتها بنجاح تسمى lexeme. - (postgresql.org)
هناك 6 أنواع رئيسية من القواميس في PostgreSQL يمكننا استخدامها لتحويل الكلمات إلى جذوعها اللغوية (lexmize):
the
, of
, عن
, على
, فـ...
).connections
, connective
, connected
, and connecting
إلى connect
).الإعداد الافتراضي to_tsvector('arabic', ...)
يستخدم فعليًا قاموس Snowball لتحويل الكلمات إلى جذوعها، وكما رأيت أعلاه، فقد حوّل "فجوات" إلى "جوا"، وهذا خطأ. يمكنك تجربة تجزئة الجذور باستخدام Snowball بلغات مختلفة في موقعهم الرسمي.
لتطبيق البحث بالنص الكامل باللغة العربية، سنستخدم ملفات بصيغة 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 يحول الكلمات إلى مرادفاتها التي حددناها في الملف. وهذا مثال على نتائج الاستعلام أثناء العمل:
قد يصبح البحث بالنص الكامل بطيئًا جدًا عندما تصبح بياناتك ضخمة جدًا، وبالتالي هناك شيئان يجب عليك القيام بهما:
CREATE INDEX arabic_col_idx ON table_name USING GIN (to_tsvector('arabic', col_name));
يمكنك استخدام فهرس GIST
أيضًا.
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', 'طيبة');
تحاول عملية الترتيب قياس مدى صلة المستندات باستعلام معين، بحيث عند وجود العديد من التطابقات، يمكن عرض الأكثر صلة أولاً. - (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;
البحث بالنص الكامل هو تقنية قوية تعزز نتائج الاستعلامات وتجربة المستخدمين. ولكن كما رأينا، فإن دعم بعض اللغات مثل اللغة العربية لا يزال محدودًا جدًا بسبب طبيعة اللغة المعقدة. ومع ذلك، إذا كنت تخطط لاستخدامه مع لغات أخرى، فستحصل على نتائج رائعة، خاصة عند دمجه مع قاموس المترادفات.
ابق على اطلاع على أحدث الأفكار والتقنيات من خلال زيارة مدونتنا