إنشاء فهرس نصي
يمكن استخدام الفهارس النصية مع أي إصدار من ClickHouse >= 26.2، بغض النظر عن إعداد التوافق.
Query
- String وFixedString،
- Array(String) وArray(FixedString)،
- Map (باستخدام الدالتين mapKeys وmapValues)، و
- JSON (باستخدام الدالتين JSONAllPaths و
JSONAllValues).
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)، يُنتج مُقسِّم الرموزsparseGramsN-grams متغيرة الطول، مما يتيح تمثيلًا أكثر مرونة للنص الأصلي. على سبيل المثال،tokenizer = sparseGrams(3, 5, 4)يُنشئ داخليًا 3- و4- و5-grams من سلسلة الإدخال، لكن لا تُعاد إلا 4- و5-grams.arrayلا يُجري أي تقسيم إلى رموز، أي إن كل قيمة في row هي رمز (راجع الدالة array).
يطبّق مُقسِّم الرموز
splitByString فواصل التقسيم من اليسار إلى اليمين.
وقد يؤدي ذلك إلى حدوث حالات التباس.
على سبيل المثال، ستؤدي سلاسل الفواصل ['%21', '%'] إلى تجزئة %21abc على هيئة ['abc']، بينما سيؤدي تبديل ترتيب سلسلتي الفواصل إلى ['%', '%21'] إلى إخراج ['21abc'].
في معظم الحالات، ستحتاج إلى أن تُفضِّل المطابقة الفواصل الأطول أولًا.
ويمكن تحقيق ذلك عمومًا عبر تمرير سلاسل الفواصل بترتيب تنازلي حسب الطول.
وإذا كانت سلاسل الفواصل تُشكّل prefix code، فيمكن تمريرها بأي ترتيب.Query
Response
asciiCJK لأنه يتعامل بشكل صحيح مع حدود الكلمات في Unicode، بما في ذلك محارف CJK.
وسيطة المعالج المسبق (اختيارية). يشير المعالج المسبق إلى تعبير يُطبَّق على سلسلة الإدخال قبل التقسيم إلى رموز.
تشمل حالات الاستخدام الشائعة لوسيطة المعالج المسبق
- تحويل الأحرف إلى صغيرة/كبيرة، أو طيّ الحالة لتمكين المطابقة غير الحساسة لحالة الأحرف، مثل lower، lowerUTF8، caseFoldUTF8.
- تطبيع UTF-8، مثل normalizeUTF8NFC، normalizeUTF8NFD، normalizeUTF8NFKC، normalizeUTF8NFKD، normalizeUTF8NFKCCasefold، toValidUTF8.
- إزالة المحارف أو السلاسل الفرعية غير المرغوب فيها أو تحويلها، مثل علامات التشكيل، مثل extractTextFromHTML، substring، idnaEncode، translate، removeDiacriticsUTF8.
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, [...]).
لذلك، ولأفضل تجربة استخدام، نوصي باستخدام تعبيرات المعالج المسبق.SETTINGS use_skip_indexes = 0).
على سبيل المثال،
Query
Query
Query
Query
- تصفية كلمات التوقف (رموز شديدة التكرار). فالـرموز الشائعة جدًا مثل “the” و”a” و”is” تحمل قيمة محدودة من حيث صلة البحث وتؤدي إلى تضخيم الفهرس.
يمكنك استخدام المعالج اللاحق لاستبعادها بتحويلها إلى رموز فارغة — وتُتجاهل الـرموز الفارغة، أي لا تُضاف إلى الفهرس.
مثال:
if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str) - إزالة الطابع الزمني. كثيرًا ما تبدأ أسطر السجل بطابع زمني منظَّم مثل
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).
- نهج المعالج اللاحق: استخدم مُجزِّئ
- التجذير. يؤدي ربط كل رمز بجذره إلى تحسين شمولية البحث عبر مطابقة المتغيرات الصرفية التي تشترك في الجذر نفسه.
على سبيل المثال، عند استخدام التجذير للغة الإنجليزية، فإن “running” و”runs” و”run” جميعها تُختزل إلى “run”، لذا فإن query عن أي من هذه المتغيرات يطابقها جميعًا.
يوفّر ClickHouse الدالة stem مدمجة لعدة لغات.
مثال:
stem(str, 'en') - تطبيع حالة الأحرف. تحويل الـرموز إلى أحرف صغيرة أو كبيرة لتمكين case-insensitive matching، مثل lower وlowerUTF8. بالنسبة إلى التحويل إلى الأحرف الصغيرة أو الكبيرة، نوصي باستخدام معالج مسبق بدلًا من معالج لاحق.
Array(String)، يظل المُعالج اللاحق يعمل على الرموز المميزة الفردية بوصفها قيمًا من نوع String العادي.
يُحظر استخدام الدوال غير الحتمية.
يُطبَّق المُعالج اللاحق على كل رمز مميّز يُولَّد أثناء إنشاء الفهرس (وبالنسبة إلى المُجزِّئ array، يُعدّ كل عنصر في المصفوفة رمزًا مميّزًا). وعند وقت تنفيذ الاستعلام، يعتمد السلوك على الدالة:
- بالنسبة إلى
hasTokenوhasAllTokensوhasAnyTokensوhasPhrase(مع أي مُجزِّئ مدعوم): يُطبَّق المُعالج اللاحق على كلٍّ من الرموز المميزة في النص المراد البحث داخله وعبارة البحث، مما يتيح مطابقة مُطبَّعة بالكامل (مثل البحث غير الحسّاس لحالة الأحرف). وبالنسبة إلىhasPhrase، تُرتَّب الرموز المميزة بعد المعالجة اللاحقة بشكل متجاور، لذا فإن أي رمز مميّز يحذفه المُعالج اللاحق لا يترك فجوة موضعية، وتظل العبارة مطابقة عبره — على سبيل المثال، مع مُعالج لاحق لكلمات التوقف يحذفthe، فإنhasPhrase(col, 'see cat')يطابق المستندsee the cat. - بالنسبة إلى جميع الدوال الأخرى (
=,IN,has,hasAny,hasAll,mapContains*): لا تُطبَّق المعالجة اللاحقة إلا على عبارة البحث لأغراض lookup لتلميح الفهرس؛ بينما يظل المسند على مستوى الصف يقارن بقيم العمود الأصلية.
- إزالة كلمات التوقف باستخدام تعبير مُعالج لاحق:
- أزِل الطوابع الزمنية باستخدام تعبير المعالجة اللاحقة:
- أزِل الطوابع الزمنية باستخدام تعبير المعالجة المسبقة:
- أزل الطوابع الزمنية باستخدام تعبير يجمع بين معالج مسبق ومعالج لاحق:
- اشتقاق جذور التوكنات باستخدام تعبير للمعالجة اللاحقة:
=, 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 (الافتراضي) للاحتفاظ بتخزين يعتمد على قائمة الترحيل فقط؛ وتبقى الفهارس النصية التي أُنشئت دون هذه الوسيطة بلا مواضع.
حبيبية الفهرس.
تُنفَّذ الفهارس النصية داخل ClickHouse كنوع من فهارس التخطي.
ومع ذلك، وعلى خلاف فهارس التخطي الأخرى، تستخدم الفهارس النصية حبيبية لا نهائية (100 مليون).
ويمكن ملاحظة ذلك في تعريف جدول الفهرس النصي.
مثال:
Query
Response
استخدام فهرس نصي
نوصي باستخدام الدالتين
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
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.hasAnyTokens and hasAllTokens
hasPhrase
hasAllTokens، التي تشترط فقط وجود جميع الرموز في أي موضع، فإن hasPhrase تشترط أن تظهر كتسلسل متصل.
تُجزَّأ عبارة البحث إلى رموز باستخدام المجزئ نفسه المُعدّ لعمود الفهرس.
وعندما يستخدم فهرس النص معالجًا لاحقًا، تُطبَّع عبارة البحث أيضًا قبل البحث في الفهرس.
لاحظ أن الدالة تتطلب أحد مجزئات splitByNonAlpha أو splitByString أو ngrams أو asciiCJK.
مثال:
has
رمز واحدًا ضمن مصفوفة من السلاسل النصية.
مثال:
hasAny و hasAll
mapContains
mapContainsKey) مع الرموز المستخرجة من السلسلة النصية المطلوب البحث فيها ضمن مفاتيح الـ map.
ويشبه هذا السلوك الدالة equals مع عمود String.
ولا يُستخدَم فهرس نصي إلا إذا كان قد أُنشئ على expression mapKeys(map).
Example:
mapContainsValue
map.
ويشبه سلوكها سلوك الدالة equals مع عمود String.
ولا يُستخدم الفهرس النصي إلا إذا كان قد أُنشئ على التعبير mapValues(map).
مثال:
mapContainsKeyLike و mapContainsValueLike
operator[]
mapKeys(map) أو mapValues(map)، أو كليهما.
مثال:
Array(T) وMap(K, V) مع الفهرس النصي.
فهرسة أعمدة Array(String)
clickhouse) فحص جميع السجلات:
keywords في كل صف.
للتغلّب على مشكلة الأداء هذه، نُعرّف فهرسًا نصيًا للعمود keywords:
فهرسة أعمدة Map
فهرسة أعمدة JSON
JSON بثلاث طرق:
- فهارس على أعمدة فرعية محددة — أنشئ فهرسًا نصيًا على مسار JSON معروف، تمامًا كما تفعل مع عمود عادي. يؤدي ذلك إلى فهرسة القيم الموجودة في هذا المسار.
- فهارس قائمة على المسار باستخدام JSONAllPaths — تُفهرِس جميع المسارات الموجودة في كل granule لتخطي الـ granules التي لا يمكن أن تحتوي على المسار المطلوب في الاستعلام. وهذا مشابه لأعمدة
Map. - فهارس قائمة على القيم باستخدام JSONAllValues — تُفهرِس جميع القيم عبر جميع مسارات 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
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.
وفي مثل هذه الحالات، لن يتخطى الفهرس هذا الصف، وسيتولى عامل التصفية على بيانات العمود الفعلية إجراء التقييم النهائي.
وهذا هو السلوك نفسه في حالات استخدام الفهرس النصي الأخرى، حيث يعمل الفهرس كعامل تصفية تمهيدي سريع.إنشاء الفهرس
أنماط الاستعلام المدعومة
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
'New weather in York') لا يتطابق لأن الرموز ليست بالترتيب الصحيح.
الصف 3 ('weather in New Orleans') لا يتطابق لأنه لا يحتوي على الرمز 'York'.
ضبط الأداء
القراءة المباشرة
direct read عن الاستعلام بالاعتماد حصريًا على فهرس النص (أي من خلال عمليات lookup في فهرس النص) من دون الوصول إلى عمود النص الأساسي.
وتقرأ عمليات lookup في فهرس النص قدرًا قليلًا نسبيًا من البيانات، لذا فهي أسرع بكثير من فهارس التخطي المعتادة في ClickHouse (التي تُجري lookup في فهرس التخطي، ثم تحميل الحبيبات المتبقية وتصفيتها).
يُتحكَّم في direct read عبر إعدادين:
- الإعداد query_plan_direct_read_from_text_index (وقيمته الافتراضية true) الذي يحدد ما إذا كانت
direct readمفعّلة بشكل عام. - كان الإعداد use_skip_indexes_on_data_read شرطًا مسبقًا لـ
direct readفي إصدارات ClickHouse الأقدم من < 26.4.
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
__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
__text_index_...) إلى شرط التصفية.
وبفضل تحسين PREWHERE، يُقسَّم شرط التصفية إلى ثلاثة أجزاء اقترانية منفصلة، تُطبَّق بترتيب تصاعدي بحسب التعقيد الحسابي.
بالنسبة لهذا الاستعلام، يكون ترتيب التطبيق هو __text_index_...، ثم greaterOrEquals(...)، وأخيرًا like(...).
ويتيح هذا الترتيب تخطي عدد أكبر من حبيبات البيانات مقارنةً بما يتخطاه الفهرس النصي وشرط التصفية الأصلي، وذلك قبل قراءة الأعمدة الثقيلة المستخدمة في الاستعلام بعد عبارة WHERE، مما يقلل بدرجة أكبر كمية البيانات المطلوب قراءتها.
استعلامات 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.
التخزين المؤقت
إعدادات ذاكرة التخزين المؤقت لرموز الفهرس النصي
إعدادات ذاكرة التخزين المؤقت للترويسات
إعدادات ذاكرة التخزين المؤقت لقوائم الترحيل
القيود
- قد يستهلك البناء المادي للفهرسة النصية التي تحتوي على عدد كبير من الرموز (مثلًا 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
bloom_filter وngrambf_v1 وtokenbf_v1 وsparse_grams)، إلا أن كليهما يختلفان اختلافًا جوهريًا من حيث التصميم وحالات الاستخدام المستهدفة:
فهارس Bloom filter
- تستند إلى هياكل بيانات احتمالية قد تؤدي إلى false positives.
- لا يمكنها إلا الإجابة عن أسئلة الانتماء إلى مجموعة، أي إن العمود قد يحتوي على الرمز X أو أنه بالتأكيد لا يحتوي على X.
- تخزّن معلومات على مستوى الحبيبة، مما يتيح تخطي نطاقات واسعة أثناء تنفيذ query.
- يصعب ضبطها على نحو صحيح (راجع هنا للاطلاع على مثال).
- وهي مدمجة نسبيًا (بضعة كيلوبايتات أو ميغابايتات لكل part).
- تبني فهرسًا معكوسًا حتميًا على الرموز. ولا يمكن أن تنتج عن الفهرس نفسه false positives.
- مُحسّنة خصيصًا لأعباء عمل البحث النصي.
- تخزّن معلومات على مستوى الصف، مما يتيح lookup فعّالًا للمصطلحات.
- وهي كبيرة نسبيًا (من عشرات إلى مئات الميغابايتات لكل part).
- فهي لا تدعم tokenization وpreprocessing المتقدمين.
- وهي لا تدعم البحث باستخدام عدة رموز.
- وهي لا توفر خصائص الأداء المتوقعة من فهرس معكوس.
- فهي توفر tokenization وpreprocessing
- وتوفر دعمًا فعّالًا لـ
hasAllTokensوLIKEوmatchووظائف البحث النصي المشابهة. - وتتمتع بقابلية توسّع أفضل بكثير مع المجموعات النصية الكبيرة.
تفاصيل التنفيذ
- قاموس يربط كل رمز بقائمة ترحيلات، و
- مجموعة من قوائم الترحيلات، تمثّل كل واحدة منها مجموعة من أرقام الصفوف.
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
hackernews:
ALTER TABLE لإضافة فهرس نصي إلى عمود comment، ثم نطبّقه فعليًا:
hasToken وhasAnyTokens وhasAllTokens.
ستُظهر الأمثلة التالية الفرق الكبير في الأداء بين فحص فهرس تقليدي وتحسين القراءة المباشرة.
1. استخدام hasToken
hasToken مما إذا كان النص يحتوي على رمز واحد محدد.
سنبحث عن الرمز الحساس لحالة الأحرف ‘ClickHouse’.
القراءة المباشرة معطّلة (المسح القياسي)
بشكل افتراضي، يستخدم ClickHouse فهرس التخطي لتصفية الحبيبات، ثم يقرأ بيانات العمود الخاصة بهذه الحبيبات.
يمكننا محاكاة هذا السلوك من خلال تعطيل القراءة المباشرة.
2. استخدام hasAnyTokens
hasAnyTokens مما إذا كان النص يحتوي على واحدة على الأقل من الوحدات المعطاة.
سنبحث عن التعليقات التي تحتوي على ‘love’ أو ‘ClickHouse’.
تم تعطيل Direct read (Standard scan)
3. استخدام hasAllTokens
hasAllTokens من احتواء النص على جميع الرموز المعطاة.
سنبحث عن التعليقات التي تحتوي على كلٍّ من ‘love’ و’ClickHouse’.
القراءة المباشرة معطّلة (المسح القياسي)
حتى مع تعطيل القراءة المباشرة، يظل فهرس التخطي القياسي فعّالًا.
فهو يقلّص عدد الصفوف من 28.7 مليون صف إلى 147.46 ألف صف فقط، لكنه لا يزال مضطرًا إلى قراءة 57.03 ميغابايت من العمود.
4. البحث المركب: OR, AND, NOT, …
hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) هي الخيار المفضل والأكثر كفاءة.
- مدونة: الإعلان عن الإتاحة العامة للبحث النصي الكامل في ClickHouse
- مدونة: بناء بحث نصي كامل عالي الأداء لتخزين الكائنات
- فيديو: مقدمة إلى البحث النصي الكامل في ClickHouse
- فيديو: ما وراء الكواليس: البحث النصي الكامل في ClickHouse على نطاق واسع وبسرعة عالية
- عرض تقديمي: نظرة داخلية على البحث النصي الكامل في ClickHouse: سريع وأصلي وعمودي
- عرض تقديمي: فهارس قواعد البيانات المقلوبة: لماذا، وما هي، وكيف تعمل، FOSDEM 2026