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
وسائط أخرى (اختيارية). حبيبية الفهرس. تُنفَّذ الفهارس النصية داخل 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 أو أكبر منه. مثال على فهرس نصي يستخدم مجزئ splitByNonAlpha:
في هذا المثال، لا يُعدّ clickhouse إلا رمزًا واحدًا. أما support فلا يُعدّ رمزًا لأنه يمكن أن يطابق support وsupports وsupporting وغيرها. للعثور على جميع rows التي تبدأ بـ clickhouse supports، يُرجى إنهاء نمط البحث بمسافة لاحقة:
وبالمثل، ينبغي استخدام endsWith مع مسافة في البداية:

hasToken و hasTokenOrNull

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

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:
يدعم فهرس النص البحث عن العبارات عبر الدالة hasPhrase. يجب أن تظهر جميع الرموز في العبارة بشكل متتالٍ وبالترتيب نفسه داخل المستند. يُسرّع فهرس النص البحث عن العبارات من خلال تقاطع قوائم الترحيل الخاصة بجميع الرموز في العبارة لتحديد الحبيبات المرشحة. وداخل تلك الحبيبات، يتحقق ClickHouse بعد ذلك من التجاور الدقيق بين الرموز. تتوافق hasPhrase مع المُجزِّئات ‏splitByNonAlpha وsplitByString وngrams وasciiCJK. تُجزَّأ سلسلة العبارة إلى رموز باستخدام المُجزِّئ المُهيأ في الفهرس. ويتم تجاهل أحرف الفصل الخاصة بالـ المُجزِّئ في العبارة: ‏hasPhrase(text, 'quick+brown') تكافئ hasPhrase(text, 'quick brown') بالنسبة إلى الـ المُجزِّئ ‏splitByNonAlpha.

مثال

Query
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، فتُقسَّم إلى عدة كتل تُخزَّن تسلسليًا في ملف قوائم الترحيلات. دمج الفهارس النصية عند دمج 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']) هي الخيار المفضل والأكثر كفاءة. محتوى قديم
آخر تعديل في ٢٥ يونيو ٢٠٢٦