Skip to main content
تتيح الفهارس النصية (المعروفة أيضًا باسم الفهارس المعكوسة) إجراء بحث نصي كامل سريع في البيانات النصية. يخزن الفهرس النصي تعيينًا من الرموز إلى أرقام الصفوف التي تحتوي على كل رمز. وتُنشأ الرموز عبر عملية تُسمى تقسيم النص إلى رموز. على سبيل المثال، يحوّل مُجزِّئ النص الافتراضي في ClickHouse الجملة الإنجليزية “The cat likes mice.” إلى الرموز [“The”, “cat”, “likes”, “mice”]. على سبيل المثال، افترض وجود جدول يحتوي على عمود واحد وثلاثة صفوف
التوكنات المقابلة هي:
عادةً ما نفضّل البحث بغضّ النظر عن حالة الأحرف، لذلك نحوّل التوكنات إلى أحرف صغيرة:
سنزيل أيضًا كلمات الحشو مثل “I” و”the” و”and” لأنها تتكرر في كل صف تقريبًا:
وعليه، يحتوي الفهرس النصي (من حيث التصور) على المعلومات التالية:
عند توفير رمز بحث، تتيح بنية الفهرس هذه العثور بسرعة على جميع الصفوف المطابقة.

إنشاء فهرس نصي

أصبحت الفهارس النصية متاحةً بشكل عام (GA) في ClickHouse الإصدار 26.2 والإصدارات الأحدث. في هذه الإصدارات، لا حاجة إلى تهيئة أي إعدادات خاصة لاستخدام الفهرس النصي. نوصي بشدة باستخدام إصدارات ClickHouse ‏>= 26.2 في بيئات الإنتاج.
يمكن استخدام الفهارس النصية مع أي إصدار من ClickHouse ‏>= 26.2، بغض النظر عن إعداد التوافق.
لإنشاء فهرس نصي، استخدم الصياغة التالية:
Query
يمكن تعريف الفهارس النصية على أعمدة من الأنواع التالية: كما أن الأعمدة من النوع Nullable(T) وLowCardinality() مدعومة أيضًا، بما في ذلك Array(Nullable(String or FixedString)). بدلًا من ذلك، لإضافة فهرس نصي إلى جدول موجود:
Query
إذا أضفت فهرسًا إلى جدول موجود، فنوصي بإنشاء الفهرس فعليًا لأجزاء الجدول الحالية (وإلا فسيعتمد البحث في الأجزاء التي لا تحتوي على فهرس على عمليات مسح شاملة بطيئة).
Query
لإزالة فهرس نصي، يُرجى تنفيذ
Query
الوسيطة tokenizer (إلزامية). تحدد الوسيطة tokenizer مُقسِّم الرموز:
  • splitByNonAlpha يقسم السلاسل النصية عند محارف ASCII غير الأبجدية الرقمية (راجع الدالة splitByNonAlpha).
  • splitByString(S) يقسم السلاسل النصية باستخدام سلاسل فاصلة S يحددها المستخدم (راجع الدالة splitByString). يمكن تحديد الفواصل باستخدام معلمة اختيارية، على سبيل المثال: tokenizer = splitByString([', ', '; ', '\n', '\\']). لاحظ أن كل سلسلة يمكن أن تتكون من عدة محارف (', ' في المثال). قائمة الفواصل الافتراضية، إذا لم تُحدَّد صراحةً (على سبيل المثال: tokenizer = splitByString)، هي مسافة بيضاء واحدة [' '].
  • asciiCJK يقسم السلاسل النصية إلى رموز وفقًا لقواعد حدود الكلمات في Unicode (على غرار Unicode Text Segmentation (UAX #29)). وتُشكِّل محارف ASCII الأبجدية الرقمية والشرطات السفلية رموزًا مع الموصلات (ASCII : للحروف، و. و' للمحارف من النوع نفسه). أما محارف Unicode غير التابعة لـ ASCII، بما في ذلك محارف CJK، فتصبح رموزًا من محرف واحد.
  • ngrams(N) يقسم السلاسل النصية إلى n-grams متساوية الحجم بطول N (راجع الدالة ngrams). يمكن تحديد طول ngram باستخدام معلمة عدد صحيح اختيارية بين 1 و8، على سبيل المثال: tokenizer = ngrams(3). حجم ngram الافتراضي، إذا لم يُحدَّد صراحةً (على سبيل المثال: tokenizer = ngrams)، هو 3.
  • sparseGrams(min_length, max_length, min_cutoff_length) يقسم السلاسل النصية إلى n-grams متغيرة الطول، بحيث لا يقل طولها عن min_length ولا يزيد على max_length (شاملًا) من المحارف (راجع الدالة sparseGrams). ما لم يُحدَّد ذلك صراحةً، تكون القيم الافتراضية لـ min_length وmax_length هي 3 و100. إذا تم توفير المعلمة min_cutoff_length، فلن تُعاد إلا n-grams التي يكون طولها أكبر من أو مساويًا لـ min_cutoff_length. مقارنةً بـ ngrams(N)، يُنتج مُقسِّم الرموز sparseGrams ‏N-grams متغيرة الطول، مما يتيح تمثيلًا أكثر مرونة للنص الأصلي. على سبيل المثال، tokenizer = sparseGrams(3, 5, 4) يُنشئ داخليًا 3- و4- و5-grams من سلسلة الإدخال، لكن لا تُعاد إلا 4- و5-grams.
  • array لا يُجري أي تقسيم إلى رموز، أي إن كل قيمة في row هي رمز (راجع الدالة array).
جميع مُقسِّمات الرموز المتاحة مُدرجة في system.tokenizers.
يطبّق مُقسِّم الرموز splitByString فواصل التقسيم من اليسار إلى اليمين. وقد يؤدي ذلك إلى حدوث حالات التباس. على سبيل المثال، ستؤدي سلاسل الفواصل ['%21', '%'] إلى تجزئة %21abc على هيئة ['abc']، بينما سيؤدي تبديل ترتيب سلسلتي الفواصل إلى ['%', '%21'] إلى إخراج ['21abc']. في معظم الحالات، ستحتاج إلى أن تُفضِّل المطابقة الفواصل الأطول أولًا. ويمكن تحقيق ذلك عمومًا عبر تمرير سلاسل الفواصل بترتيب تنازلي حسب الطول. وإذا كانت سلاسل الفواصل تُشكّل prefix code، فيمكن تمريرها بأي ترتيب.
لفهم كيفية قيام مُقسِّم الرموز بتقسيم سلسلة الإدخال، يمكنك استخدام الدالتين tokens وtokensForLikePattern: مثال:
Query
Response
العمل مع مدخلات غير ASCII. يمكن إنشاء فهرس نصي على بيانات نصية بأي لغة وبأي مجموعة محارف. بالنسبة إلى النصوص غير ASCII، يُوصى باستخدام مُقسِّم الرموز asciiCJK لأنه يتعامل بشكل صحيح مع حدود الكلمات في Unicode، بما في ذلك محارف CJK. وسيطة المعالج المسبق (اختيارية). يشير المعالج المسبق إلى تعبير يُطبَّق على سلسلة الإدخال قبل التقسيم إلى رموز. تشمل حالات الاستخدام الشائعة لوسيطة المعالج المسبق
  1. تحويل الأحرف إلى صغيرة/كبيرة، أو طيّ الحالة لتمكين المطابقة غير الحساسة لحالة الأحرف، مثل lower، lowerUTF8، caseFoldUTF8.
  2. تطبيع UTF-8، مثل normalizeUTF8NFC، normalizeUTF8NFD، normalizeUTF8NFKC، normalizeUTF8NFKD، normalizeUTF8NFKCCasefold، toValidUTF8.
  3. إزالة المحارف أو السلاسل الفرعية غير المرغوب فيها أو تحويلها، مثل علامات التشكيل، مثل extractTextFromHTML، substring، idnaEncode، translate، removeDiacriticsUTF8.
يجب أن يحوّل تعبير المعالج المسبق قيمة إدخال من النوع String أو FixedString إلى قيمة من النوع نفسه. إذا كان الفهرس النصي مبنيًا على عمود من النوع Nullable(T) أو LowCardinality(T)، فيجب أن يقبل تعبير المعالج المسبق القيم القابلة للإبطال أو منخفضة الكاردينالية (أي ألّا يطرح استثناءً). أمثلة:
  • INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))
  • INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = substringIndex(col, '\n', 1))
  • INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(extractTextFromHTML(col)))
  • INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = removeDiacriticsUTF8(caseFoldUTF8(col)))
كذلك، يجب ألا يشير تعبير المعالج المسبق إلا إلى العمود أو التعبير المعرَّف فوقه الفهرس النصي. أمثلة:
  • INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = upper(lower(col)))
  • INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(lower(col), lower(col)))
  • غير مسموح: INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(col, col))
يُحظر استخدام الدوال غير الحتمية.
المعالجات المسبقة، من حيث المبدأ، تكافئ تغليف عمود الفهرس أو التعبير بتعبير المعالج المسبق. على سبيل المثال، يمكن محاكاة المعالج المسبق lower في INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col)) باستخدام INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha'). وعيب الصيغة الأخيرة هو أن المعالج المسبق المُحاكى لا يُطبَّق إلا إذا طابق شرط التصفية في بند WHERE. على سبيل المثال، يطابق WHERE hasAllTokens(lower(col), [...]) بينما لا يطابق WHERE hasAllTokens(col, [...]). لذلك، ولأفضل تجربة استخدام، نوصي باستخدام تعبيرات المعالج المسبق.
تستخدم الدوال hasToken، وhasAllTokens، وhasAnyTokens، وhasPhrase المعالج المسبق لتحويل مصطلح البحث أولًا قبل تقسيمه إلى رموز. لاحظ أنه نظرًا إلى أن المعالج المسبق لا يُطبَّق إلا على مسار الفهرس النصي، فقد تختلف نتائج هذه الدوال بين الاستعلامات التي تستخدم الفهرس النصي والاستعلامات التي لا تستخدمه (مثل SETTINGS use_skip_indexes = 0). على سبيل المثال،
Query
يعادل ما يلي:
Query
في هذه الحالة، يُحوِّل تعبير المعالج المسبق عناصر المصفوفة، كل عنصر على حدة. مثال:
Query
لتعريف معالج مسبق في فهرس نصي على أعمدة من النوع Map عند الإنشاء، يجب على المستخدمين تحديد ما إذا كان الفهرس يُبنى على مفاتيح الـMap أم على قيمه. مثال:
Query
وسيطة المعالج اللاحق (اختيارية). يشير المعالج اللاحق إلى تعبير يُطبَّق على كل رمز ناتج بعد عملية tokenization. وعلى خلاف المعالج المسبق، الذي يُحوِّل سلسلة الإدخال بالكامل قبل أن يقسّمها الـمُجزِّئ إلى رموز، فإن المعالج اللاحق يعمل على الـرموز نفسها، واحدةً تلو الأخرى. وهذا هو الموضع الطبيعي للتحويلات التي تكون بطبيعتها على مستوى الـرمز. تشمل حالات الاستخدام الشائعة لوسيطة المعالج اللاحق ما يلي:
  1. تصفية كلمات التوقف (رموز شديدة التكرار). فالـرموز الشائعة جدًا مثل “the” و”a” و”is” تحمل قيمة محدودة من حيث صلة البحث وتؤدي إلى تضخيم الفهرس. يمكنك استخدام المعالج اللاحق لاستبعادها بتحويلها إلى رموز فارغة — وتُتجاهل الـرموز الفارغة، أي لا تُضاف إلى الفهرس. مثال: if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str)
  2. إزالة الطابع الزمني. كثيرًا ما تبدأ أسطر السجل بطابع زمني منظَّم مثل 2024-01-15T10:23:45 أو تتضمنه. تؤدي فهرسة رموز الطابع الزمني إلى تضخيم الفهرس بسلاسل لا تحمل أي صلة بالبحث. توجد طريقتان متكاملتان لتجاهل الطوابع الزمنية:
    • نهج المعالج اللاحق: استخدم مُجزِّئ ‏splitByString (التقسيم حسب المسافات البيضاء) بحيث يصبح الطابع الزمني بالكامل رمزًا واحدًا، ثم استخدم parseDateTimeOrNull لاكتشافه واستبعاده. مثال: if(isNull(parseDateTimeOrNull(str, '%Y-%m-%dT%H:%i:%S')), str, '') بالنسبة إلى الطوابع الزمنية التي تتضمن إزاحات timezone أو أجزاءً من الثانية، استخدم parseDateTimeBestEffortOrNull(str) من دون سلسلة format صريحة.
    • نهج المعالج المسبق: أزل الطابع الزمني من سطر السجل الكامل قبل عملية tokenization باستخدام regular expression. مثال: replaceRegexpAll(str, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '') ينجح هذا مع أي مُجزِّئ ويكون أكثر كفاءة لأن أحرف الطابع الزمني لا تخضع لعملية tokenization أصلًا. ويمكن الجمع بين النهجين: إذ يزيل المعالج المسبق الطابع الزمني، بينما يُطبِّع المعالج اللاحق الـرموز المتبقية أو يصفّيها (مثل التحويل إلى أحرف صغيرة + حذف كلمات الشدة مثل ERROR أو INFO).
  3. التجذير. يؤدي ربط كل رمز بجذره إلى تحسين شمولية البحث عبر مطابقة المتغيرات الصرفية التي تشترك في الجذر نفسه. على سبيل المثال، عند استخدام التجذير للغة الإنجليزية، فإن “running” و”runs” و”run” جميعها تُختزل إلى “run”، لذا فإن query عن أي من هذه المتغيرات يطابقها جميعًا. يوفّر ClickHouse الدالة stem مدمجة لعدة لغات. مثال: stem(str, 'en')
  4. تطبيع حالة الأحرف. تحويل الـرموز إلى أحرف صغيرة أو كبيرة لتمكين case-insensitive matching، مثل lower وlowerUTF8. بالنسبة إلى التحويل إلى الأحرف الصغيرة أو الكبيرة، نوصي باستخدام معالج مسبق بدلًا من معالج لاحق.
يُحوِّل تعبير المُعالج اللاحق الرموز المميزة من نوع String إلى رموز مميزة من النوع ذاته. كما يجب أن يشير تعبير المُعالج اللاحق فقط إلى العمود أو التعبير الذي يُعرَّف فوقه فهرس النص. وعندما يكون العمود من نوع Array(String)، يظل المُعالج اللاحق يعمل على الرموز المميزة الفردية بوصفها قيمًا من نوع String العادي. يُحظر استخدام الدوال غير الحتمية. يُطبَّق المُعالج اللاحق على كل رمز مميّز يُولَّد أثناء إنشاء الفهرس (وبالنسبة إلى المُجزِّئ array، يُعدّ كل عنصر في المصفوفة رمزًا مميّزًا). وعند وقت تنفيذ الاستعلام، يعتمد السلوك على الدالة:
  • بالنسبة إلى hasToken وhasAllTokens وhasAnyTokens وhasPhrase (مع أي مُجزِّئ مدعوم): يُطبَّق المُعالج اللاحق على كلٍّ من الرموز المميزة في النص المراد البحث داخله وعبارة البحث، مما يتيح مطابقة مُطبَّعة بالكامل (مثل البحث غير الحسّاس لحالة الأحرف). وبالنسبة إلى hasPhrase، تُرتَّب الرموز المميزة بعد المعالجة اللاحقة بشكل متجاور، لذا فإن أي رمز مميّز يحذفه المُعالج اللاحق لا يترك فجوة موضعية، وتظل العبارة مطابقة عبره — على سبيل المثال، مع مُعالج لاحق لكلمات التوقف يحذف the، فإن hasPhrase(col, 'see cat') يطابق المستند see the cat.
  • بالنسبة إلى جميع الدوال الأخرى (=, IN, has, hasAny, hasAll, mapContains*): لا تُطبَّق المعالجة اللاحقة إلا على عبارة البحث لأغراض lookup لتلميح الفهرس؛ بينما يظل المسند على مستوى الصف يقارن بقيم العمود الأصلية.
أمثلة:
  • إزالة كلمات التوقف باستخدام تعبير مُعالج لاحق:
  • أزِل الطوابع الزمنية باستخدام تعبير المعالجة اللاحقة:
  • أزِل الطوابع الزمنية باستخدام تعبير المعالجة المسبقة:
  • أزل الطوابع الزمنية باستخدام تعبير يجمع بين معالج مسبق ومعالج لاحق:
  • اشتقاق جذور التوكنات باستخدام تعبير للمعالجة اللاحقة:
دعم الدوال. بالنسبة إلى الشروط التي تستعين بالفهرس النصي، تُطبَّق المعالجة المسبقة والمعالجة اللاحقة على قيمة البحث قبل الفحص على مستوى granule، بحيث يستخدم البحث في الفهرس الرموز نفسها التي خُزِّنت عند إنشاء الفهرس. في معظم الدوال (=, IN, startsWith, endsWith, LIKE, mapContains*)، لا يُستخدم الفهرس النصي إلا لتخطي كتل البيانات غير ذات الصلة؛ ويظل ClickHouse يتحقق من كل صف متبقٍّ باستخدام الشرط الأصلي على بيانات العمود الأصلية. بالنسبة إلى دوال البحث عن الرموز (hasToken, hasAllTokens, hasAnyTokens)، يكون الفهرس النصي هو مسار التقييم الأساسي: يطبّع ClickHouse الـ needle باستخدام المعالجة المسبقة نفسها، ومُقسِّم الرموز، والمعالجة اللاحقة التي طُبِّقت عند إنشاء الفهرس، ويستخدم هذه الصيغة المُطبَّعة لكلٍّ من أجزاء الجدول المفهرسة وغير المفهرسة. ومع وجود معالجة لاحقة، تُطبَّع أيضًا رموز الـ haystack وقت الاستعلام (مع أي مُقسِّم رموز، وليس فقط array)، بحيث يخضع جانبا المقارنة للتحويل نفسه على نحو متسق، ولا تعتمد النتيجة على ما إذا كان الفهرس يُقرأ مباشرةً (الإعداد query_plan_direct_read_from_text_index) أو ما إذا كان جزء معيّن يحتوي على فهرس materialized — على سبيل المثال، تمكين المطابقة غير الحساسة لحالة الأحرف في hasAllTokens(col, ['FOO']) باستخدام معالجة لاحقة lower. من دون support_phrase_search، تستخدم hasPhrase الفهرس على سبيل الاستدلال فقط، وتتحقق من كل صف متبقٍّ باستخدام الشرط الأصلي؛ وتقوم المعالجة اللاحقة أيضًا بتطبيع كلٍّ من العبارة ورموز الـ haystack بالطريقة نفسها، بحيث تكون النتيجة مستقلة عن مسار القراءة، ولا تؤدي الرموز التي تُسقطها المعالجة اللاحقة إلى كسر تجاور العبارة. ومع support_phrase_search = 1، تستخدم hasPhrase قراءات مباشرة دقيقة (مع الاستمرار في تطبيق المعالجة اللاحقة، إن وُجدت). تُتجاهل رموز البحث التي تحوّلها المعالجة اللاحقة إلى سلسلة فارغة، أي تُعامَل على أنها غير موجودة في عبارة البحث. ¹ يستخدم LIKE وmatch القراءة المباشرة كتلميح مع مُقسِّمات الرموز المذكورة، وإلا فيلجآن إلى المسح الشامل. ويدعم LIKE أيضًا القراءة المباشرة (من دون تلميح) (مفعّلة عبر use_text_index_like_evaluation_by_dictionary_scan) مع مُقسِّمَي الرموز splitByNonAlpha وarray من دون معالج مسبق أو لاحق. ² لا يدعم ILIKE إلا عبر القراءة المباشرة (من دون تلميح) (use_text_index_like_evaluation_by_dictionary_scan = 1، مع مُقسِّم الرموز splitByNonAlpha أو array). ولا يوجد مسار بديل لاستخدام الفهرس كتلميح: إذا كان هذا الإعداد معطّلًا أو لم يكن مُقسِّم الرموز ضمن المجموعة المدعومة، فلن يُستخدَم الفهرس مع ILIKE. ويجب أن يكون المعالج المسبق، إن وُجد، هو lower أو upper؛ أما المعالجات اللاحقة فغير مدعومة. تجريبي: دعم وسيطة البحث عن العبارة (اختيارية). المعلمة التجريبية support_phrase_search (الافتراضي: 0) تتحكم في ما إذا كان الفهرس يخزّن مواضع الـtoken. عند ضبطها على 1، يخزّن الفهرس أيضًا بيانات المواضع (في ملف .pos)، مما يتيح المطابقة الدقيقة للعبارات عبر القراءات المباشرة للدالة hasPhrase. يؤدي تخزين المواضع إلى زيادة حجم الفهرس على القرص ورفع تكلفة الكتابة، لذا فهو خيار اختياري. تنسيق التخزين على القرص ليس مستقرًا بعد، لذلك فهذه المعلمة تجريبية وقد تتغير في إصدار مستقبلي. لذلك، يتطلب إنشاء فهرس مع support_phrase_search = 1 تمكين إعداد MergeTree allow_experimental_text_index_phrase_search. اضبط support_phrase_search = 0 (الافتراضي) للاحتفاظ بتخزين يعتمد على قائمة الترحيل فقط؛ وتبقى الفهارس النصية التي أُنشئت دون هذه الوسيطة بلا مواضع.
هذه الوسيطة تجريبية ويجب استخدامها للاختبار فقط. اضبط إعداد MergeTree allow_experimental_text_index_phrase_search لتمكين تخزين المواضع.
حبيبية الفهرس. تُنفَّذ الفهارس النصية داخل ClickHouse كنوع من فهارس التخطي. ومع ذلك، وعلى خلاف فهارس التخطي الأخرى، تستخدم الفهارس النصية حبيبية لا نهائية (100 مليون). ويمكن ملاحظة ذلك في تعريف جدول الفهرس النصي. مثال:
Query
Response
يضمن الارتفاع الكبير جدًا في حبيبية الفهرس إنشاء فهرس نصي للجزء بالكامل. ويُتجاهل أي حبيبية الفهرس مُحدَّدة صراحةً.

استخدام فهرس نصي

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

الدوال المدعومة

يمكن استخدام فهرس نصي إذا استُخدمت دوال النص في بند WHERE أو بنود PREWHERE:
= (equals) يطابق مصطلح البحث المعطى بالكامل. مثال:

IN

IN (in) يشبه equals، لكنه يطابق كل مصطلحات البحث. مثال:
NOT IN (notIn) غير مدعوم في الفهرس النصي.

LIKE and match

تستخدم هذه الدوال حاليًا الفهرس النصي للتصفية فقط إذا كان الـ مجزئ في الفهرس هو splitByNonAlpha أو ngrams أو sparseGrams.
لا يدعم الفهرس النصي NOT LIKE (notLike).
لاستخدام LIKE (like) والدالة match مع الفهارس النصية، يجب أن يتمكن ClickHouse من استخراج رموز كاملة من عبارة البحث. وبالنسبة إلى الفهرس الذي يستخدم مجزئ من النوع ngrams، يتحقق ذلك إذا كان طول السلاسل النصية المطلوب البحث عنها بين أحرف البدل مساويًا لطول ngram أو أكبر منه. Example لفهرس نصي يستخدم مجزئ من النوع splitByNonAlpha:
يمكن أن يطابق support في المثال كلمات مثل support وsupports وsupporting وغيرها. هذا النوع من الاستعلامات هو استعلام عن سلسلة فرعية، ولا يمكن تسريعه باستخدام فهرس نصي. لاستفادة من فهرس نصي في استعلامات LIKE، يجب إعادة كتابة نمط LIKE على النحو التالي:
تضمن المسافات الواقعة إلى يسار support ويمينه إمكانية استخراج هذا المصطلح على أنه رمز. ولحسن الحظ، توجد حالة خاصة يمكن فيها لـ ClickHouse الاستفادة من الفهرس المقلوب لتسريع استعلامات LIKE بشكل كبير. راجع قسم ضبط أداء LIKE/ILIKE لمزيد من التفاصيل.

multiSearchAny و multiMatchAny

يختبر multiSearchAny وصيغته الخاصة بـ UTF-8، multiSearchAnyUTF8، ما إذا كانت أيٌّ من عدة سلاسل فرعية حرفية تظهر في النص المُراد البحث فيه، بينما يختبر multiMatchAny ما إذا كان أيٌّ من عدة تعبيرات نمطية يطابق. تستخدم هذه الدوال الفهرس النصي بالشروط نفسها التي تستخدمها LIKE و match (انظر أعلاه): يجب أن يتمكن ClickHouse من استخراج رموز كاملة من كل needle، ويجب أن تكون قائمة needles ثابتة. تُقرأ granule إذا كان من المحتمل أن تحتوي على أي needle. بالنسبة إلى multiMatchAny، إذا تعذر اختزال نمط واحد إلى متطلب رمز (على سبيل المثال .*، الذي يطابق أي مستند)، فلن يمكن استخدام الفهرس النصي، وسيرجع الاستعلام إلى فحص كامل. كما هو الحال مع LIKE و match، يعمل البحث بالسلاسل الفرعية والتعبيرات النمطية بأفضل شكل مع مجزئات ngrams و sparseGrams. تفهرس مجزئات النص هذه n-grams متداخلة من الأحرف، لذلك يُحلَّل needle إلى n-grams موجودة في الفهرس أينما ظهر needle كسلسلة فرعية، بغض النظر عمّا إذا كان يبدأ أو ينتهي في منتصف كلمة. وبالتالي يمكن استخدام needle كما هو، ما دام طوله لا يقل عن حجم n-gram. Example للفهرس النصي مع مجزئ ngrams:
وعلى النقيض من ذلك، لا يفهرس المُجزِّئ splitByNonAlpha سوى الرموز الكاملة (أي الكلمات الكاملة). ولأن needle قد يبدأ أو ينتهي في منتصف كلمة، فإن ClickHouse يتجاهل الرمزين الأول والأخير في كل needle، بحيث لا يمكن للفهرس تقليم الحبيبات إلا بالاعتماد على الرموز الكاملة. ولكي يستخدم البحث عن substring والبحث بالتعبيرات النمطية الفهرس مع splitByNonAlpha، أَحِط كل needle بمحارف فاصلة (مثل المسافات) بحيث يُكوِّن رمزًا كاملًا واحدًا أو أكثر. مثال على الفهرس النصي مع المُجزِّئ splitByNonAlpha:

startsWith and endsWith

على غرار LIKE، لا يمكن للدالتين startsWith وendsWith استخدام فهرس نصي إلا إذا أمكن استخراج رموز كاملة من عبارة البحث. وبالنسبة إلى الفهرس الذي يستخدم مجزئ ngrams، يتحقق ذلك إذا كان طول السلاسل النصية المطلوب البحث عنها بين أحرف البدل مساويًا لطول ngram أو أكبر منه. وعندما يستخدم الفهرس النصي معالجًا لاحقًا، يظل بإمكان هاتين الدالتين استخدام الفهرس في وضع Hint إذا بقيت رموز التلميح المستخرجة غير فارغة بعد التطبيع. وإذا أدى التطبيع إلى إسقاط جميع رموز التلميح، فلن يُستخدم الفهرس لهذا المسند. مثال على فهرس نصي يستخدم مجزئ splitByNonAlpha:
في هذا المثال، لا يُعدّ clickhouse إلا رمزًا واحدًا. أما support فلا يُعدّ رمزًا لأنه يمكن أن يطابق support وsupports وsupporting وغيرها. للعثور على جميع rows التي تبدأ بـ clickhouse supports، يُرجى إنهاء نمط البحث بمسافة لاحقة:
وبالمثل، ينبغي استخدام endsWith مع مسافة في البداية:

hasToken

يبدو استخدام الدالة hasToken بسيطًا، لكنه ينطوي على بعض الجوانب التي قد تُسبب مشكلات عند استخدامها في عمليات البحث في فهارس النص مع مُقسِّمات الرموز غير splitByNonAlpha و/أو تعبيرات المعالجة المسبقة/اللاحقة. نوصي باستخدام hasAnyTokens و hasAllTokens بدلًا من ذلك.الصيغ غير الحساسة لحالة الأحرف hasTokenCaseInsensitive و hasTokenCaseInsensitiveOrNull ليست مدركة لفهرس النص — فهي تعمل دائمًا على شكل فحص كامل للصفوف حتى على الأعمدة المفهرسة نصيًا. ولإجراء مطابقة غير حساسة لحالة الأحرف، استخدم معالجًا مسبقًا أو لاحقًا من نوع lower(...) واجمعه مع hasToken / hasAllTokens / hasAnyTokens.
تُطابِق الدالة hasToken رمزًا واحدًا محددًا. وعلى عكس الدوال المذكورة سابقًا، فإنها لا تُجزِّئ مصطلح البحث إلى رموز (إذ تفترض أن المُدخل عبارة عن رمز واحد). مثال:

hasAnyTokens and hasAllTokens

تُطابِق الدالتان hasAnyTokens وhasAllTokens أيًّا من الرموز المعطاة أو جميعها. تقبل هاتان الدالتان رموز البحث إما كسلسلة نصية ستُجزَّأ إلى رموز باستخدام نفس المجزئ المستخدم لعمود الفهرس، أو كمصفوفة من الرموز المُعالجة مسبقًا، والتي لن يُجرى عليها أي تقسيم قبل البحث. راجع توثيق الدالة لمزيد من المعلومات. مثال:

hasPhrase

تُطابِق الدالة hasPhrase عبارةً ما: إذ يجب أن تظهر جميع الرموز بشكل متتالٍ وبالترتيب نفسه كما في سلسلة البحث. وعلى خلاف hasAllTokens، التي تشترط فقط وجود جميع الرموز في أي موضع، فإن hasPhrase تشترط أن تظهر كتسلسل متصل. تُجزَّأ عبارة البحث إلى رموز باستخدام المجزئ نفسه المُعدّ لعمود الفهرس. وعندما يستخدم فهرس النص معالجًا لاحقًا، تُطبَّع عبارة البحث أيضًا قبل البحث في الفهرس. لاحظ أن الدالة تتطلب أحد مجزئات splitByNonAlpha أو splitByString أو ngrams أو asciiCJK. مثال:

has

تُطابِق دالة المصفوفات has رمز واحدًا ضمن مصفوفة من السلاسل النصية. مثال:

hasAny و hasAll

تتحقق دوال المصفوفات hasAny و hasAll مما إذا كان عمود المصفوفة المفهرس يحتوي على أيٍّ من مجموعة ثابتة من سلاسل البحث أو عليها كلها. مثال:

mapContains

تُطابِق الدالة mapContains (وهي alias لـ mapContainsKey) مع الرموز المستخرجة من السلسلة النصية المطلوب البحث فيها ضمن مفاتيح الـ map. ويشبه هذا السلوك الدالة equals مع عمود String. ولا يُستخدَم فهرس نصي إلا إذا كان قد أُنشئ على expression ‏mapKeys(map). Example:

mapContainsValue

تُطابِق الدالة mapContainsValue الرموز المميزة المستخرجة من السلسلة النصية المطلوب البحث فيها داخل قيم map. ويشبه سلوكها سلوك الدالة equals مع عمود String. ولا يُستخدم الفهرس النصي إلا إذا كان قد أُنشئ على التعبير mapValues(map). مثال:

mapContainsKeyLike و mapContainsValueLike

تُطابِق الدالتان mapContainsKeyLike و mapContainsValueLike نمطًا مع جميع مفاتيح Map أو قيمه (على الترتيب). مثال:

operator[]

يمكن استخدام عامل الوصول operator[] مع الفهرس النصي لتصفية المفاتيح والقيم. ولا يُستخدم الفهرس النصي إلا إذا أُنشئ على التعبيرين mapKeys(map) أو mapValues(map)، أو كليهما. مثال:
راجع الأمثلة التالية لاستخدام الأعمدة من النوع Array(T) وMap(K, V) مع الفهرس النصي.

فهرسة أعمدة Array(String)

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

فهرسة أعمدة Map

في العديد من حالات استخدام observability، تُقسَّم رسائل السجل إلى “مكوّنات” وتُخزَّن باستخدام أنواع البيانات المناسبة، مثلًا التاريخ والوقت للطابع الزمني، وenum لمستوى السجل، وما إلى ذلك. من الأفضل تخزين حقول المقاييس كأزواج مفتاح-قيمة. تحتاج فرق العمليات إلى البحث بكفاءة في السجلات لأغراض استكشاف الأخطاء وإصلاحها، والحوادث الأمنية، والمراقبة. خذ جدول السجلات هذا على سبيل المثال:
من دون فهرس نصي، يتطلب البحث في بيانات Map إجراء مسح كامل للجدول:
مع ازدياد حجم السجلات، تصبح هذه الاستعلامات بطيئة. يتمثل الحل في إنشاء فهرس نصي لمفاتيح Map وقيمها. استخدم mapKeys لإنشاء فهرس نصي عندما تحتاج إلى العثور على السجلات بناءً على أسماء الحقول أو أنواع السمات:
استخدم mapValues لإنشاء فهرس نصي عندما تحتاج إلى البحث في المحتوى الفعلي للسمات نفسها:
أمثلة على الاستعلامات:

فهرسة أعمدة JSON

يمكن استخدام الفهارس النصية مع أعمدة JSON بثلاث طرق:
  1. فهارس على أعمدة فرعية محددة — أنشئ فهرسًا نصيًا على مسار JSON معروف، تمامًا كما تفعل مع عمود عادي. يؤدي ذلك إلى فهرسة القيم الموجودة في هذا المسار.
  2. فهارس قائمة على المسار باستخدام JSONAllPaths — تُفهرِس جميع المسارات الموجودة في كل granule لتخطي الـ granules التي لا يمكن أن تحتوي على المسار المطلوب في الاستعلام. وهذا مشابه لأعمدة Map.
  3. فهارس قائمة على القيم باستخدام JSONAllValues — تُفهرِس جميع القيم عبر جميع مسارات JSON لتسريع البحث النصي الكامل في أي عمود JSON فرعي باستخدام فهرس واحد.

فهارس على أعمدة فرعية محددة

يمكنك إنشاء فهرس تخطٍّ على أي عمود فرعي في JSON باستخدام الصياغة نفسها المستخدمة مع الأعمدة العادية. توجد طريقتان للإشارة إلى عمود JSON فرعي في تعبير الفهرس:
  • مسار محدد النوع مُعرَّف في تلميح نوع JSON — ويمكن الوصول إليه بالاسم مباشرةً: json.a.
  • مسار Dynamic مع تحويل نوع صريح — استخدم صياغة تحويل النوع ::: json.b::String.
مثال على تعريف الفهرس:
Query
مثال على استعلام:
Query
Response
مثال لاستعلام:
Query
Response

الفهارس المستندة إلى المسار باستخدام JSONAllPaths

على غرار أعمدة Map، يمكن إنشاء فهارس نصية على أعمدة JSON باستخدام JSONAllPaths. يخزّن الفهرس مجموعة مسارات JSON الموجودة في كل حبيبة، ويستخدمها لتخطّي الحبيبات التي لا يحتوي فيها الاستعلام على المسار المطلوب. مثال على تعريف الفهرس:
Query
يمكنك استخدام EXPLAIN indexes = 1 للتحقق من استخدام فهرس التخطي. عندما يكون المسار موجودًا في جزء واحد فقط، يتجاوز الفهرس الجزء الآخر. مثال:
Query
Response
عندما لا يوجد المسار في أي جزء، تُتخطى جميع الأجزاء والحبيبات. مثال:
Query
Response
يستخدم IS NOT NULL الفهرس أيضًا — إذ يتخطى الحبيبات التي يغيب فيها المسار (لأن القيمة ستكون NULL): مثال:
Query
Response

الفهارس القائمة على القيم باستخدام JSONAllValues

يمكن استخدام الفهارس النصية لتسريع عمليات البحث في أعمدة JSON عبر الدالة JSONAllValues. تعيد JSONAllValues جميع القيم من عمود JSON بصيغة Array(String). وتُحوَّل قيم أنواع البيانات غير النصية (مثل الأعداد الصحيحة والمصفوفات) إلى تمثيلها النصي. ويفهرس فهرس نصي مُنشأ باستخدام JSONAllValues هذه التمثيلات النصية عبر جميع مسارات JSON في كل صف. ويمكن لهذا الفهرس بعد ذلك تسريع الاستعلامات التي تُطبِّق عامل تصفية على أعمدة JSON الفرعية الفردية. وعندما يطبّق استعلام عامل تصفية على عمود فرعي محدد (مثل data.user_name = 'alice')، يمكن للفهرس النصي أن يتخطى بسرعة الصفوف (والحبيبات) التي لا تحتوي أيٌّ من قيم JSON فيها على رموز البحث.
قد يُنتج الفهرس نتائج إيجابية كاذبة عندما تحتوي مسارات JSON مختلفة على الرموز نفسها. على سبيل المثال، إذا كان الصف 1 يحتوي على {"a": "hello", "b": "world"} وكان الاستعلام يبحث عن data.a = 'world'، فلن يتمكن الفهرس النصي من تمييز أن world تنتمي إلى المسار b لا إلى a. وفي مثل هذه الحالات، لن يتخطى الفهرس هذا الصف، وسيتولى عامل التصفية على بيانات العمود الفعلية إجراء التقييم النهائي. وهذا هو السلوك نفسه في حالات استخدام الفهرس النصي الأخرى، حيث يعمل الفهرس كعامل تصفية تمهيدي سريع.
إنشاء الفهرس
مثال على تعريف الفهرس:
أنماط الاستعلام المدعومة
بمجرد إنشاء الفهرس، يمكنه تسريع الاستعلامات على الأعمدة الفرعية في JSON باستخدام الدوال نفسها المستخدَمة مع أعمدة String، بالإضافة إلى الدالة equals لجميع الأعمدة. الوصول إلى العمود الفرعي:
الوصول إلى العمود الفرعي باستخدام CAST الصريح:
المعامل IN:
على سبيل المثال، بحث عادي باستخدام فهرس نصي
يطابق جميع الصفوف التي تحتوي على التوكنات المحددة بأي ترتيب. في المثال، يطابق الصف While she stayed in Tokyo, the weather was great. شرط التصفية. في المقابل، يعني البحث بالعبارة مطابقة التوكنات بالترتيب المحدد. على سبيل المثال،
يطابق أي صف يحتوي على تسلسل الرموز weather in Tokyo، مثل How is the weather in Tokyo?؟ يُسرّع الفهرس النصي البحث عن العبارات من خلال تقاطع قوائم الإسناد لجميع الرموز في العبارة لتحديد الحبيبات المرشحة. ثم يتحقق ClickHouse داخل تلك الحبيبات من التجاور الدقيق بين الرموز. هذه العملية مكلفة نسبيًا وأبطأ من استعلامات البحث النصي العادية. لتسريع استعلامات البحث عن العبارات، يُرجى تمكين تخزين المواضع في الفهرس النصي (راجع Optional parameters أعلاه). يمكن استخدام hasPhrase مع مُقسِّمات الرموز splitByNonAlpha وsplitByString وngrams وasciiCJK. تُقسَّم سلسلة العبارة المعطاة إلى رموز باستخدام مُقسِّم الرموز الخاص بالفهرس. تُتجاهل أحرف الفصل في العبارة: hasPhrase(text, 'quick+brown') مكافئ لـ hasPhrase(text, 'quick brown')، بافتراض استخدام splitByNonAlpha كمُقسِّم للرموز.

مثال

Query
Response
الصف 2 ('New weather in York') لا يتطابق لأن الرموز ليست بالترتيب الصحيح. الصف 3 ('weather in New Orleans') لا يتطابق لأنه لا يحتوي على الرمز 'York'.

ضبط الأداء

القراءة المباشرة

يمكن تسريع بعض أنواع الاستعلامات النصية بدرجة كبيرة بفضل تحسين يُعرف باسم “القراءة المباشرة”. مثال:
تُجيب آلية تحسين direct read عن الاستعلام بالاعتماد حصريًا على فهرس النص (أي من خلال عمليات lookup في فهرس النص) من دون الوصول إلى عمود النص الأساسي. وتقرأ عمليات lookup في فهرس النص قدرًا قليلًا نسبيًا من البيانات، لذا فهي أسرع بكثير من فهارس التخطي المعتادة في ClickHouse (التي تُجري lookup في فهرس التخطي، ثم تحميل الحبيبات المتبقية وتصفيتها). يُتحكَّم في direct read عبر إعدادين: الدوال المدعومة تدعم آلية تحسين direct read الدوال hasToken وhasAllTokens وhasAnyTokens. إذا كان فهرس النص معرّفًا باستخدام المُجزِّئ array، فإن direct read تدعم أيضًا الدوال equals وhas وhasAny وhasAll وmapContainsKey وmapContainsValue. ويمكن أيضًا دمج هذه الدوال باستخدام عوامل التشغيل AND وOR وNOT. كما يمكن أن تتضمن عبارتا WHERE أو PREWHERE عوامل تصفية إضافية لا تتعلق بدوال البحث النصي (لأعمدة النص أو الأعمدة الأخرى) - وفي هذه الحالة ستظل آلية تحسين direct read مستخدمة، ولكن بفعالية أقل (إذ إنها تنطبق فقط على دوال البحث النصي المدعومة). للتحقق مما إذا كان الاستعلام يستخدم direct read، شغّل الاستعلام باستخدام EXPLAIN PLAN actions = 1. وعلى سبيل المثال، استعلام مع تعطيل direct read
القيمة المُعادة
في حين يُشغَّل الاستعلام نفسه مع query_plan_direct_read_from_text_index = 1
القيمة المعادة
يحتوي خرج EXPLAIN PLAN الثاني على عمود افتراضي __text_index_<index_name>_<function_name>_<id>. إذا كان هذا العمود موجودًا، فهذا يعني أنه تم استخدام القراءة المباشرة. إذا كانت عبارة التصفية في WHERE تحتوي فقط على دوال البحث النصي، فيمكن للاستعلام تجنّب قراءة بيانات العمود بالكامل وتحقيق أكبر فائدة أداء عبر القراءة المباشرة. ومع ذلك، حتى إذا جرى الوصول إلى العمود النصي في موضع آخر من الاستعلام، فستظل القراءة المباشرة توفّر تحسينًا في الأداء. القراءة المباشرة كتلميح تعتمد القراءة المباشرة كتلميح على المبادئ نفسها التي تعتمد عليها القراءة المباشرة العادية، لكنها تضيف بدلًا من ذلك عامل تصفية إضافيًا مُنشأً من بيانات فهرس النص، من دون الاستغناء عن العمود النصي الأساسي. وتُستخدم مع الدوال التي قد تؤدي فيها القراءة من فهرس النص فقط إلى مطابقات إيجابية كاذبة. الدوال المدعومة هي: like, startsWith, endsWith, equals, has, hasPhrase, mapContainsKey, و mapContainsValue. يمكن لعامل التصفية الإضافي أن يوفّر انتقائية إضافية لتقييد مجموعة النتائج بدرجة أكبر عند دمجه مع عوامل تصفية أخرى، مما يساعد على تقليل كمية البيانات المقروءة من الأعمدة الأخرى. تخضع القراءة المباشرة كتلميح للإعداد query_plan_text_index_add_hint (مُمكّن افتراضيًا). مثال على استعلام من دون تلميح:
القيم المعادة
بينما يُنفَّذ الاستعلام نفسه مع query_plan_text_index_add_hint = 1
القيمة المُعادة
في ناتج EXPLAIN PLAN الثاني، يمكنك ملاحظة أنه قد أُضيف جزء اقتراني إضافي (__text_index_...) إلى شرط التصفية. وبفضل تحسين PREWHERE، يُقسَّم شرط التصفية إلى ثلاثة أجزاء اقترانية منفصلة، تُطبَّق بترتيب تصاعدي بحسب التعقيد الحسابي. بالنسبة لهذا الاستعلام، يكون ترتيب التطبيق هو __text_index_...، ثم greaterOrEquals(...)، وأخيرًا like(...). ويتيح هذا الترتيب تخطي عدد أكبر من حبيبات البيانات مقارنةً بما يتخطاه الفهرس النصي وشرط التصفية الأصلي، وذلك قبل قراءة الأعمدة الثقيلة المستخدمة في الاستعلام بعد عبارة WHERE، مما يقلل بدرجة أكبر كمية البيانات المطلوب قراءتها.

استعلامات LIKE/ILIKE

عندما يكون نمط استعلام LIKE/ILIKE هو %<alpha-numeric-characters-without-spaces>% ويكون مُجزِّئ لفهرس النص text index هو splitByNonAlpha أو array، يستفيد ClickHouse من الفهرس المعكوس inverted index لتسريع استعلامات LIKE/ILIKE بشكل كبير. ولتحقيق ذلك، يفحص ClickHouse القاموس Dictionary الخاص بالفهرس المعكوس بدلًا من إجراء فحص كامل للجدول للعثور على النمط المطابق. عند تمكين هذا التحسين، يُفترض أن تصبح استعلامات LIKE/ILIKE أسرع بكثير من الفحص الكامل للجدول. ومع ذلك، إذا كان النمط يطابق معظم الرموز tokens في القاموس، فقد يصبح الأداء أسوأ مقارنةً بالفحص الكامل للجدول. ولحسن الحظ، توجد آلية احتياطية fallback تمنع ذلك. يخضع هذا التحسين لإعداد واحد: وتخضع الآلية الاحتياطية fallback لإعدادين: لا يدعم هذا التحسين إلا الدالتين like و ilike.

التخزين المؤقت

توجد ذاكرات تخزين مؤقت مختلفة على مستوى الخادم للاحتفاظ بأجزاء من الفهرس النصي في الذاكرة (انظر قسم تفاصيل التنفيذ): توجد حالياً ذاكرات تخزين مؤقت للترويسات بعد فك تسلسلها، والرموز، وقوائم الترحيل الخاصة بالفهرس النصي لتقليل عمليات الإدخال/الإخراج. استخدم الإعدادات use_text_index_header_cache وuse_text_index_tokens_cache وuse_text_index_postings_cache لتعطيل قراءة الاستعلامات من كل ذاكرة تخزين مؤقت على حدة والكتابة إليها. لمسح ذاكرات التخزين المؤقت، استخدم العبارة SYSTEM CLEAR TEXT INDEX CACHES يرجى الرجوع إلى إعدادات الخادم التالية لضبط ذاكرات التخزين المؤقت.

إعدادات ذاكرة التخزين المؤقت لرموز الفهرس النصي

إعدادات ذاكرة التخزين المؤقت للترويسات

إعدادات ذاكرة التخزين المؤقت لقوائم الترحيل

القيود

للفهرس النصي حاليًا القيود التالية:
  • قد يستهلك البناء المادي للفهرسة النصية التي تحتوي على عدد كبير من الرموز (مثلًا 10 مليارات رمز) كميات كبيرة من الذاكرة. ويمكن أن يحدث البناء المادي للفهرس النصي مباشرةً (ALTER TABLE <table> MATERIALIZE INDEX <index>) أو بصورة غير مباشرة أثناء عمليات دمج الأجزاء.
  • لا يمكن إجراء البناء المادي للفهرسة النصية على الأجزاء التي تحتوي على أكثر من 4.294.967.296 (= 2^32 = نحو 4.2 مليارات) صف. ومن دون فهرس نصي مُنشأ ماديًا، تلجأ الاستعلامات إلى البحث البطيء بالقوة الغاشمة داخل الجزء. وكتقدير لأسوأ الاحتمالات، افترض أن جزءًا يحتوي على عمود واحد من النوع String وأن إعداد MergeTree max_bytes_to_merge_at_max_space_in_pool (القيمة الافتراضية: 150 جيجابايت) لم يتغير. في هذه الحالة، يحدث ذلك إذا كان العمود يحتوي في المتوسط على أقل من 29.5 حرفًا لكل صف. وعمليًا، تحتوي الجداول أيضًا على أعمدة أخرى، وتكون العتبة أقل من ذلك بعدة مرات (بحسب عدد الأعمدة الأخرى ونوعها وحجمها).

فهارس النص مقابل الفهارس المعتمدة على Bloom filter

يمكن تسريع predicates الخاصة بـ String باستخدام فهارس النص والفهارس المعتمدة على Bloom filter (أنواع الفهارس bloom_filter وngrambf_v1 وtokenbf_v1 وsparse_grams)، إلا أن كليهما يختلفان اختلافًا جوهريًا من حيث التصميم وحالات الاستخدام المستهدفة: فهارس Bloom filter
  • تستند إلى هياكل بيانات احتمالية قد تؤدي إلى false positives.
  • لا يمكنها إلا الإجابة عن أسئلة الانتماء إلى مجموعة، أي إن العمود قد يحتوي على الرمز X أو أنه بالتأكيد لا يحتوي على X.
  • تخزّن معلومات على مستوى الحبيبة، مما يتيح تخطي نطاقات واسعة أثناء تنفيذ query.
  • يصعب ضبطها على نحو صحيح (راجع هنا للاطلاع على مثال).
  • وهي مدمجة نسبيًا (بضعة كيلوبايتات أو ميغابايتات لكل part).
فهارس النص
  • تبني فهرسًا معكوسًا حتميًا على الرموز. ولا يمكن أن تنتج عن الفهرس نفسه false positives.
  • مُحسّنة خصيصًا لأعباء عمل البحث النصي.
  • تخزّن معلومات على مستوى الصف، مما يتيح lookup فعّالًا للمصطلحات.
  • وهي كبيرة نسبيًا (من عشرات إلى مئات الميغابايتات لكل part).
تدعم الفهارس المعتمدة على Bloom filter البحث النصي الكامل فقط كـ “أثر جانبي”:
  • فهي لا تدعم tokenization وpreprocessing المتقدمين.
  • وهي لا تدعم البحث باستخدام عدة رموز.
  • وهي لا توفر خصائص الأداء المتوقعة من فهرس معكوس.
أما فهارس النص، فعلى النقيض، فهي مصممة خصيصًا للبحث النصي الكامل:
  • فهي توفر tokenization وpreprocessing
  • وتوفر دعمًا فعّالًا لـ hasAllTokens وLIKE وmatch ووظائف البحث النصي المشابهة.
  • وتتمتع بقابلية توسّع أفضل بكثير مع المجموعات النصية الكبيرة.

تفاصيل التنفيذ

يتكوّن كل فهرس نصي من بنيتَي بيانات (مجرّدتين):
  • قاموس يربط كل رمز بقائمة ترحيلات، و
  • مجموعة من قوائم الترحيلات، تمثّل كل واحدة منها مجموعة من أرقام الصفوف.
يُنشأ الفهرس النصي للجزء بأكمله. وعلى خلاف skip indexes الأخرى، يمكن دمج الفهرس النصي بدلًا من إعادة بنائه عند دمج data parts (انظر أدناه). أثناء إنشاء الفهرس، تُنشأ ثلاثة ملفات (لكل جزء): ملف كتل القاموس (.dct) تُرتَّب الرموز في الفهرس النصي وتُخزَّن في كتل قاموس تضم 512 رمزًا لكل منها (حجم الكتلة قابل للتهيئة عبر parameter ‏dictionary_block_size). ويتكوّن ملف كتل القاموس (.dct) من جميع كتل القاموس لكل index granules في الجزء. ملف ترويسة الفهرس (.idx) يحتوي ملف ترويسة الفهرس، لكل كتلة قاموس، على أول رمز في الكتلة وإزاحته النسبية داخل ملف كتل القاموس. تشبه بنية sparse index هذه فهرس المفتاح الأساسي المتناثر) في ClickHouse. ملف قوائم الترحيلات (.pst) تُرتَّب قوائم الترحيلات الخاصة بجميع الرموز ترتيبًا تسلسليًا في ملف قوائم الترحيلات. ولتوفير المساحة مع الحفاظ على سرعة عمليات التقاطع والاتحاد، تُخزَّن قوائم الترحيلات على هيئة roaring bitmaps. إذا كانت قائمة الترحيلات أكبر من posting_list_block_size، فتُقسَّم إلى عدة كتل تُخزَّن تسلسليًا في ملف قوائم الترحيلات. ملف المواضع (.pos) اختياري، فقط إذا كانت index argument ‏support_phrase_search = 1. يخزّن مواضع الرموز داخل الصفوف المطابقة. دمج الفهارس النصية عند دمج data parts، لا يحتاج الفهرس النصي إلى إعادة بنائه من الصفر؛ بل يمكن دمجه بكفاءة في step منفصلة من merge process. وخلال هذه step، تُقرأ القواميس المرتبة للفهارس النصية لكل جزء إدخال وتُدمج في قاموس موحّد جديد. كما يُعاد حساب أرقام الصفوف في قوائم الترحيلات لتعكس مواضعها الجديدة في data part المدمج، باستخدام mapping من أرقام الصفوف القديمة إلى الجديدة يُنشأ خلال Phase الدمج الأولية. وتشبه طريقة دمج الفهارس النصية هذه كيفية دمج projections التي تحتوي على العمود _part_offset. وإذا لم يكن الفهرس materialized في الجزء المصدر، فيُبنى ويُكتب في ملف مؤقت ثم يُدمج مع الفهارس من الأجزاء الأخرى ومن ملفات الفهارس المؤقتة الأخرى. تصحيح الأخطاء يمكن استخدام table function ‏mergeTreeTextIndex لفحص الفهارس النصية داخليًا.

مثال: مجموعة بيانات Hacker News

لنلقِ نظرة على تحسينات الأداء التي توفّرها الفهارس النصية في مجموعة بيانات كبيرة تضم قدرًا كبيرًا من النصوص. سنستخدم 28.7 مليون صف من التعليقات على موقع Hacker News الشهير. فيما يلي الجدول من دون فهرس نصي:
توجد 28.7 مليون صف في ملف Parquet على S3 — لِنُدرجها في جدول hackernews:
سنستخدم ALTER TABLE لإضافة فهرس نصي إلى عمود comment، ثم نطبّقه فعليًا:
الآن، لنشغّل استعلامات باستخدام الدوال hasToken وhasAnyTokens وhasAllTokens. ستُظهر الأمثلة التالية الفرق الكبير في الأداء بين فحص فهرس تقليدي وتحسين القراءة المباشرة.

1. استخدام hasToken

يتحقق hasToken مما إذا كان النص يحتوي على رمز واحد محدد. سنبحث عن الرمز الحساس لحالة الأحرف ‘ClickHouse’. القراءة المباشرة معطّلة (المسح القياسي) بشكل افتراضي، يستخدم ClickHouse فهرس التخطي لتصفية الحبيبات، ثم يقرأ بيانات العمود الخاصة بهذه الحبيبات. يمكننا محاكاة هذا السلوك من خلال تعطيل القراءة المباشرة.
القراءة المباشرة مفعّلة (قراءة سريعة من الفهرس) نُشغِّل الآن الاستعلام نفسه مع تفعيل القراءة المباشرة (وهو الإعداد الافتراضي).
استعلام direct read أسرع بأكثر من 45 مرة (0.362s مقابل 0.008s)، ويعالج بيانات أقل بكثير (9.51 GB مقابل 3.15 MB) عبر القراءة من الفهرس فقط.

2. استخدام hasAnyTokens

يتحقق hasAnyTokens مما إذا كان النص يحتوي على واحدة على الأقل من الوحدات المعطاة. سنبحث عن التعليقات التي تحتوي على ‘love’ أو ‘ClickHouse’. تم تعطيل Direct read (Standard scan)
القراءة المباشرة مفعّلة (قراءة سريعة للفهرس)
التحسّن في السرعة هنا أكثر وضوحًا في بحث “OR” الشائع هذا. أصبح الاستعلام أسرع بنحو 89 مرة تقريبًا (1.329s مقابل 0.015s) بفضل تجنّب إجراء مسح كامل للعمود.

3. استخدام hasAllTokens

يتحقق hasAllTokens من احتواء النص على جميع الرموز المعطاة. سنبحث عن التعليقات التي تحتوي على كلٍّ من ‘love’ و’ClickHouse’. القراءة المباشرة معطّلة (المسح القياسي) حتى مع تعطيل القراءة المباشرة، يظل فهرس التخطي القياسي فعّالًا. فهو يقلّص عدد الصفوف من 28.7 مليون صف إلى 147.46 ألف صف فقط، لكنه لا يزال مضطرًا إلى قراءة 57.03 ميغابايت من العمود.
تم تفعيل direct read (قراءة سريعة للفهرس) يُجيب direct read عن الاستعلام بالاعتماد على بيانات الفهرس، فلا يقرأ سوى 147.46 KB.
في عملية البحث هذه باستخدام “AND”، يكون تحسين القراءة المباشرة أسرع بأكثر من 26 مرة (0.184s مقابل 0.007s) من المسح القياسي لفهرس التخطي. يسري تحسين direct read أيضًا على التعبيرات المنطقية المركبة. سنُجري هنا بحثًا غير حساس لحالة الأحرف عن ‘ClickHouse’ OR ‘clickhouse’. مع تعطيل direct read (المسح القياسي)
القراءة المباشرة مفعّلة (قراءة سريعة من الفهرس)
من خلال دمج النتائج المستخرجة من الفهرس، يصبح استعلام القراءة المباشرة أسرع بمقدار 34 مرة (0.450s مقابل 0.013s)، ويتجنب قراءة 9.58 GB من بيانات الأعمدة. في هذه الحالة تحديدًا، ستكون الصياغة hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) هي الخيار المفضل والأكثر كفاءة. محتوى قديم
آخر تعديل في ٢٣ يوليو ٢٠٢٦