انتقل إلى المحتوى

البحث النصي الكامل

البحث النصي الكامل (Full-text search) هو طريقة قوية للبحث عن النصوص في قاعدة البيانات. يجب أن تكون على دراية بكيفية عمل الفهارس، ولكن دعنا نراجع الأساسيات.

يعمل الفهرس (index) كجدول بحث، مما يسمح لمحرك الاستعلام بالعثور على السجلات بقيمة معينة بسرعة. على سبيل المثال، إذا كان لديك حقل title في كائنك، يمكنك إنشاء فهرس على هذا الحقل لتسريع العثور على الكائنات ذات العنوان المحدد.

لماذا البحث النصي الكامل مفيد؟

يمكنك بسهولة البحث عن النصوص باستخدام الفلاتر (filters). هناك العديد من عمليات السلسلة النصية (string operations) مثل .startsWith() و .contains() و .matches(). المشكلة في الفلاتر هي أن وقت تشغيلها هو O(n) حيث n هو عدد السجلات في المجموعة. عمليات السلسلة النصية مثل .matches() مكلفة بشكل خاص.

:::tip البحث النصي الكامل أسرع بكثير من الفلاتر، ولكن الفهارس لها بعض القيود. في هذه الوصفة، سنستكشف كيفية التغلب على هذه القيود. :::

مثال أساسي

الفكرة هي نفسها دائمًا: بدلاً من فهرسة النص بأكمله، نقوم بفهرسة الكلمات في النص حتى نتمكن من البحث عنها بشكل فردي.

دعنا ننشئ أبسط فهرس نصي كامل:

class Message {
  late int id;

  late String content;

  @Index()
  List<String> get contentWords => content.split(" ");
}

يمكننا الآن البحث عن الرسائل التي تحتوي على كلمات محددة في المحتوى:

final posts = await isar.messages
  .where()
  .contentWordsAnyEqualTo("hello")
  .findAll();

هذا الاستعلام سريع للغاية، ولكن هناك بعض المشاكل:

  1. يمكننا البحث عن كلمات كاملة فقط
  2. لا نأخذ علامات الترقيم في الاعتبار
  3. لا ندعم أحرف المسافة البيضاء الأخرى

تقسيم النص بالطريقة الصحيحة

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

يحدد ملحق يونيكود رقم 29 كيفية تقسيم النص إلى كلمات بشكل صحيح لجميع اللغات تقريبًا. إنه معقد للغاية، ولكن لحسن الحظ، يقوم Isar بالعمل الشاق نيابة عنا:

Isar.splitWords("hello world"); // -> ["hello", "world"]

Isar.splitWords("The quick (“brown”) fox can’t jump 32.3 feet, right?");
// -> ["The", "quick", "brown", "fox", "can’t", "jump", "32.3", "feet", "right"]

أريد المزيد من التحكم

سهل جداً! يمكننا تغيير الفهرس الخاص بنا لدعم مطابقة البادئة (prefix matching) والمطابقة غير الحساسة لحالة الأحرف (case-insensitive matching):

class Post {
  late int id;

  late String title;

  @Index(type: IndexType.value, caseSensitive: false)
  List<String> get titleWords => title.split(" ");
}

بشكل افتراضي، سيقوم Isar بتخزين الكلمات كقيم مجزأة (hashed values) وهي سريعة وفعالة من حيث المساحة. لكن لا يمكن استخدام التجزئات لمطابقة البادئة. باستخدام IndexType.value، يمكننا تغيير الفهرس لاستخدام الكلمات مباشرة بدلاً من ذلك. يمنحنا ذلك شرط where التالي: .titleWordsAnyStartsWith():

final posts = await isar.posts
  .where()
  .titleWordsAnyStartsWith("hel")
  .or()
  .titleWordsAnyStartsWith("welco")
  .or()
  .titleWordsAnyStartsWith("howd")
  .findAll();

أحتاج أيضًا إلى .endsWith()

بالتأكيد! سنستخدم خدعة لتحقيق مطابقة .endsWith():

class Post {
    late int id;

    late String title;

    @Index(type: IndexType.value, caseSensitive: false)
    List<String> get revTitleWords {
        return Isar.splitWords(title).map(
          (word) => word.reversed).toList()
        );
    }
}

لا تنس عكس النهاية التي تريد البحث عنها:

final posts = await isar.posts
  .where()
  .revTitleWordsAnyStartsWith("lcome".reversed)
  .findAll();

خوارزميات التجذيع (Stemming algorithms)

لسوء الحظ، لا تدعم الفهارس مطابقة .contains() (وهذا صحيح بالنسبة لقواعد البيانات الأخرى أيضًا). ولكن هناك بعض البدائل التي تستحق الاستكشاف. يعتمد الاختيار بشكل كبير على استخدامك. أحد الأمثلة هو فهرسة جذور الكلمات (word stems) بدلاً من الكلمة بأكملها.

خوارزمية التجذيع (stemming algorithm) هي عملية تطبيع لغوي يتم فيها اختزال الأشكال المختلفة للكلمة إلى شكل مشترك:

connection
connections
connective          --->   connect
connected
connecting

الخوارزميات الشائعة هي خوارزمية بورتر للتجذيع و خوارزميات سنوبول للتجذيع.

هناك أيضًا أشكال أكثر تقدمًا مثل الاشتقاق.

خوارزميات صوتية (Phonetic algorithms)

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

:::warning معظم الخوارزميات الصوتية تدعم لغة واحدة فقط. :::

ساوندكس (Soundex)

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

باستخدام هذه الخوارزمية، تعيد كل من "Robert" و "Rupert" السلسلة "R163" بينما تعطي "Rubin" "R150". وتعطي كل من "Ashcraft" و "Ashcroft" "A261".

ميتافون المزدوج (Double Metaphone)

خوارزمية ترميز ميتافون المزدوج (Double Metaphone) الصوتية هي الجيل الثاني من هذه الخوارزمية. إنها تُدخل العديد من التحسينات الأساسية في التصميم على خوارزمية ميتافون الأصلية.

تأخذ ميتافون المزدوج في الاعتبار العديد من المخالفات في اللغة الإنجليزية من أصول سلافية، جرمانية، سلتية، يونانية، فرنسية، إيطالية، إسبانية، صينية، وغيرها.