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

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

لإنشاء فهرس نصي، فعِّل أولًا الإعداد التجريبي المقابل:
يمكن تعريف فهرس نصي على عمود من النوع String، أو FixedString، أو Array(String)، أو Array(FixedString)، أو Map (باستخدام دالّتَي map ‏mapKeys وmapValues) وفقًا للصياغة التالية:
وسيطة tokenizer. تحدد وسيطة tokenizer أداة التقسيم إلى رموز:
  • splitByNonAlpha يقسم السلاسل النصية عند محارف ASCII غير الأبجدية الرقمية (راجع أيضًا الدالة splitByNonAlpha).
  • splitByString(S) يقسم السلاسل النصية باستخدام سلاسل الفواصل S التي يحددها المستخدم (راجع أيضًا الدالة splitByString). يمكن تحديد الفواصل باستخدام معلمة اختيارية، على سبيل المثال tokenizer = splitByString([', ', '; ', '\n', '\\']). لاحظ أن كل سلسلة يمكن أن تتكون من عدة محارف (', ' في المثال). قائمة الفواصل الافتراضية، إذا لم تُحدَّد صراحةً (على سبيل المثال، tokenizer = splitByString)، هي مسافة بيضاء واحدة [' '].
  • ngrams(N) يقسم السلاسل النصية إلى N-grams متساوية الحجم (راجع أيضًا الدالة ngrams). يمكن تحديد طول ngram باستخدام معلمة عددية صحيحة اختيارية بين 2 و8، على سبيل المثال tokenizer = ngrams(3). الحجم الافتراضي لـ ngram، إذا لم يُحدَّد صراحةً (على سبيل المثال، tokenizer = ngrams)، هو 3.
  • array لا يجري أي تقسيم إلى رموز، أي إن قيمة كل صف هي رمز واحد (راجع أيضًا الدالة array).
  • sparseGrams(min_length, max_length, min_cutoff_length) — يستخدم الخوارزمية نفسها كما في الدالة sparseGrams لتقسيم سلسلة نصية إلى جميع ngrams ذات الطول min_length وعدة ngrams بأطوال أكبر حتى max_length، شاملًا. إذا تم تحديد min_cutoff_length، فلن تُحفَظ في الفهرس إلا N-grams التي يكون طولها أكبر من أو مساويًا لـ min_cutoff_length. بخلاف ngrams(N)، التي تولّد فقط N-grams بطول ثابت، تنتج sparseGrams مجموعة من N-grams ذات أطوال متغيرة ضمن النطاق المحدد، مما يتيح تمثيلًا أكثر مرونة لسياق النص. على سبيل المثال، tokenizer = sparseGrams(3, 5, 4) سيولّد 3- و4- و5-grams من سلسلة الإدخال، ولن يحفظ في الفهرس إلا 4- و5-grams.
تطبّق أداة التقسيم splitByString فواصل التقسيم من اليسار إلى اليمين. وقد يؤدي ذلك إلى حالات التباس. على سبيل المثال، سلاسل الفواصل ['%21', '%'] ستؤدي إلى تقسيم %21abc إلى ['abc']، بينما سيؤدي تبديل ترتيب سلسلتي الفواصل إلى ['%', '%21'] إلى إخراج ['21abc']. في معظم الحالات، ستحتاج إلى أن تُفضَّل مطابقة الفواصل الأطول أولًا. ويمكن تنفيذ ذلك عمومًا بتمرير سلاسل الفواصل بترتيب تنازلي حسب الطول. وإذا كانت سلاسل الفواصل تشكّل prefix code، فيمكن تمريرها بأي ترتيب.
لا يُنصح حاليًا ببناء فهارس نصية فوق نصوص بلغات غير غربية، مثل الصينية. فقد تؤدي أدوات التقسيم المدعومة حاليًا إلى أحجام فهارس ضخمة وأزمنة استعلام طويلة. ونخطط إلى إضافة أدوات تقسيم متخصصة حسب اللغة في المستقبل للتعامل مع هذه الحالات بصورة أفضل.
لاختبار كيفية تقسيم المُقسِّمات النصية لسلسلة الإدخال، يمكنك استخدام دالة tokens في ClickHouse: على سبيل المثال،
القيمة المُعادة
وسيطة المُعالج المسبق. الوسيطة الاختيارية preprocessor هي تعبير يحوّل سلسلة الإدخال قبل تقسيمها إلى رموز. تشمل حالات الاستخدام الشائعة لوسيطة المُعالج المسبق ما يلي
  1. تحويل سلاسل الإدخال إلى أحرف صغيرة (أو كبيرة) لتمكين المطابقة غير الحساسة لحالة الأحرف، مثل lower وlowerUTF8، انظر المثال الأول أدناه.
  2. تطبيع UTF-8، مثل normalizeUTF8NFC وnormalizeUTF8NFD وnormalizeUTF8NFKC وnormalizeUTF8NFKD وtoValidUTF8.
  3. إزالة المحارف أو السلاسل الفرعية غير المرغوب فيها أو تحويلها، مثل extractTextFromHTML وsubstring وidnaEncode.
يجب أن يحوّل تعبير المُعالج المسبق قيمة إدخال من النوع String أو FixedString إلى قيمة من النوع نفسه. أمثلة:
  • 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))
كذلك، يجب ألا يشير تعبير المُعالج المسبق إلا إلى العمود الذي تم تعريف فهرس النص عليه. ولا يُسمح باستخدام الدوال غير الحتمية. تستخدم الدوال hasToken وhasAllTokens وhasAnyTokens المُعالج المسبق لتحويل مصطلح البحث أولًا قبل تقسيمه إلى رموز. على سبيل المثال:
يعادل ما يلي:
وسائط أخرى. تُنفَّذ الفهارس النصية في ClickHouse على هيئة فهارس ثانوية. ومع ذلك، بخلاف فهارس التخطي الأخرى، تمتلك الفهارس النصية قيمة GRANULARITY افتراضية للفهرس تبلغ 64. وقد اختيرت هذه القيمة تجريبيًا، وهي توفّر توازنًا جيدًا بين السرعة وحجم الفهرس في معظم حالات الاستخدام. يمكن للمستخدمين المتقدمين تحديد قيمة granularity مختلفة للفهرس (ولا نوصي بذلك).
ستعمل القيم الافتراضية للمعلمات المتقدمة التالية جيدًا في جميع الحالات تقريبًا. ولا نوصي بتغييرها.تحدد المعلمة الاختيارية dictionary_block_size (الافتراضي: 128) حجم كتل القاموس بالصفوف.تحدد المعلمة الاختيارية dictionary_block_frontcoding_compression (الافتراضي: 1) ما إذا كانت كتل القاموس تستخدم الترميز الأمامي كآلية ضغط.تحدد المعلمة الاختيارية max_cardinality_for_embedded_postings (الافتراضي: 16) عتبة cardinality التي يجب دونها تضمين قوائم الترحيل داخل كتل القاموس.تحدد المعلمة الاختيارية bloom_filter_false_positive_rate (الافتراضي: 0.1) معدل الإيجابيات الكاذبة لمرشح bloom الخاص بالقاموس.
يمكن إضافة الفهارس النصية إلى عمود أو إزالتها منه بعد إنشاء الجدول:

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

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

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

يمكن استخدام فهرس النص عند استخدام دوال نصية في عبارة WHERE ضمن استعلام SELECT:

= and !=

= (equals) و != (notEquals ) يطابقان مصطلح البحث المُعطى بالكامل. مثال:
يدعم فهرس النص العاملين = و!=، لكن البحث بالمساواة أو عدم المساواة لا يكون ذا معنى إلا عند استخدام أداة التقسيم إلى رموز array (إذ يجعل الفهرس يخزّن قيم الصف كاملة).

IN and NOT IN

تتشابه IN (in) وNOT IN (notIn) مع الدالتين equals وnotEquals، لكنهما تطابقان جميع عبارات البحث (IN) أو لا تطابقان أيًا منها (NOT IN). مثال:
تنطبق القيود نفسها المطبقة على = و !=، أي إن IN و NOT IN لا يكون لهما معنى إلا عند استخدام أداة التقسيم إلى رموز array.

LIKE وNOT LIKE وmatch

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

startsWith و endsWith

على غرار LIKE، لا يمكن للدالتين startsWith و endsWith استخدام فهرس نصي إلا إذا أمكن استخراج رموز كاملة من عبارة البحث. مثال:
في هذا المثال، لا يُعدّ رمز سوى clickhouse. ولا يُعدّ support رمز لأنه قد يطابق support وsupports وsupporting وما إلى ذلك. للعثور على جميع الصفوف التي تبدأ بـ clickhouse supports، أنهِ نمط البحث بمسافة في النهاية:
وبالمثل، ينبغي استخدام endsWith مع مسافة في البداية:

hasToken و hasTokenOrNull

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

hasAnyTokens and hasAllTokens

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

has

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

mapContains

الدالة mapContains(اسم مستعار لـ: mapContainsKey) تطابق رمزًا واحدًا ضمن مفاتيح الخريطة. مثال:

operator[]

يمكن استخدام operator[] كعامل وصول مع الفهرس النصي لتصفية المفاتيح والقيم. مثال:
اطّلع على الأمثلة التالية لاستخدام Array(T) وMap(K, V) مع الفهرس النصّي.

أمثلة على دعم Array وMap في الفهرس النصي.

فهرسة Array(String)

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

فهرسة Map

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

التنفيذ

بنية الفهرس

يتكوّن كل فهرس نصي من بنيتَي بيانات (مجرّدتين):
  • قاموس يربط كل token بـ posting list، و
  • مجموعة من posting lists، تمثّل كلٌّ منها مجموعة من أرقام الصفوف.
وبما أن الفهرس النصي هو skip index، فإن بنيات البيانات هذه توجد منطقيًا لكل index granule. أثناء إنشاء الفهرس، تُنشأ ثلاثة ملفات (لكل part): ملف كتل القاموس (.dct) تُرتَّب الـ tokens في كل index granule وتُخزَّن في كتل قاموس، تضم كل كتلة 128 token (حجم الكتلة قابل للتهيئة عبر parameter dictionary_block_size). ويتكوّن ملف كتل القاموس (.dct) من جميع كتل القاموس لكل index granules ضمن part. ملف index granules (.idx) يحتوي ملف index granules، لكل كتلة قاموس، على أول token في الكتلة، وإزاحتها النسبية في ملف كتل القاموس، وbloom filter لجميع الـ tokens في الكتلة. وتشبه بنية sparse index هذه فهرس المفتاح الأساسي المتناثر في ClickHouse). ويتيح bloom filter تخطّي كتل القاموس مبكرًا إذا لم يكن token المطلوب البحث عنه موجودًا في كتلة القاموس. ملف posting lists (.pst) تُرتَّب posting lists لجميع الـ tokens ترتيبًا تسلسليًا داخل ملف posting lists. ولتوفير المساحة مع السماح في الوقت نفسه بإجراء عمليتَي intersect وunion بسرعة، تُخزَّن posting lists على هيئة roaring bitmaps. وإذا كانت cardinality الخاصة بـ posting list أقل من 16 (وقابلة للتهيئة عبر parameter max_cardinality_for_embedded_postings)، فسيتم تضمينها داخل القاموس.

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

يمكن تسريع أنواع معيّنة من الاستعلامات النصية بشكل كبير باستخدام تحسين يُسمى “القراءة المباشرة”. وبشكل أكثر تحديدًا، يمكن تطبيق هذا التحسين إذا كان استعلام SELECT لا يضمّن عمود النص في الناتج. مثال:
تُجيب آلية تحسين القراءة المباشرة في ClickHouse عن الاستعلام بالاعتماد حصريًا على فهرس النص (أي عمليات البحث في فهرس النص) من دون الوصول إلى عمود النص الأساسي. وتقرأ عمليات البحث في فهرس النص قدرًا قليلًا نسبيًا من البيانات، لذا فهي أسرع بكثير من فهارس التخطي المعتادة في ClickHouse (التي تُجري بحثًا في فهرس التخطي، ثم يتبع ذلك تحميل الحبيبات المتبقية وتصفيتها). تتحكم إعدادان في القراءة المباشرة:
  • الإعداد query_plan_direct_read_from_text_index (الافتراضي: 1) الذي يحدد ما إذا كانت القراءة المباشرة مفعّلة بشكل عام.
  • الإعداد use_skip_indexes_on_data_read (الافتراضي: 1)، وهو شرط مسبق آخر للقراءة المباشرة. لاحظ أنه في قواعد بيانات ClickHouse التي تكون فيها compatibility < 25.10، يكون use_skip_indexes_on_data_read معطّلًا، لذا تحتاج إما إلى رفع قيمة إعداد compatibility أو تنفيذ SET use_skip_indexes_on_data_read = 1 صراحةً.
كذلك، يجب أن يكون فهرس النص مُنشأً فعليًا بالكامل لاستخدام القراءة المباشرة (استخدم ALTER TABLE ... MATERIALIZE INDEX لهذا الغرض). الدوال المدعومة تدعم آلية تحسين القراءة المباشرة الدوال hasToken وhasAllTokens وhasAnyTokens. ويمكن أيضًا دمج هذه الدوال باستخدام العوامل AND وOR وNOT. كما يمكن أن تحتوي عبارة WHERE على مرشحات إضافية لا تتعلق بدوال البحث النصي (لأعمدة النص أو الأعمدة الأخرى) — وفي هذه الحالة ستظل آلية تحسين القراءة المباشرة مستخدمة، لكنها ستكون أقل فعالية (إذ إنها تنطبق فقط على دوال البحث النصي المدعومة). لمعرفة ما إذا كان الاستعلام يستخدم القراءة المباشرة، شغّل الاستعلام مع EXPLAIN PLAN actions = 1. وكمثال، استعلام مع تعطيل القراءة المباشرة
القيمة المعادة
بينما يُنفَّذ الاستعلام نفسه مع query_plan_direct_read_from_text_index = 1
القيمة المُعادة
يحتوي ناتج EXPLAIN PLAN الثاني على عمود افتراضي __text_index_<index_name>_<function_name>_<id>. إذا كان هذا العمود موجودًا، فهذا يعني أنه تم استخدام القراءة المباشرة.

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

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

1. استخدام hasToken

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

2. استخدام hasAnyTokens

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

3. استخدام hasAllTokens

يتحقق hasAllTokens مما إذا كان النص يحتوي على جميع الرموز المميّزة المحددة. سنبحث عن التعليقات التي تحتوي على كلٍّ من ‘love’ و’ClickHouse’. القراءة المباشرة معطّلة (المسح القياسي) حتى مع تعطيل القراءة المباشرة، يظل فهرس التخطي القياسي فعّالًا. فهو يقلّص عدد الصفوف من 28.7 مليون صف إلى 147.46 ألف صف فقط، لكنه لا يزال بحاجة إلى قراءة 57.03 ميغابايت من العمود.
القراءة المباشرة مفعّلة (قراءة سريعة من الفهرس) تُجيب القراءة المباشرة عن الاستعلام بالاعتماد على بيانات الفهرس، ولا تقرأ سوى 147.46 KB.
في عملية البحث “AND” هذه، يكون تحسين القراءة المباشرة أسرع بأكثر من 26 مرة (0.184s مقابل 0.007s) من المسح القياسي لفهرس التخطي.

4. البحث المركّب: OR، AND، NOT، …

ينطبق تحسين القراءة المباشرة أيضًا على التعبيرات المنطقية المركّبة. هنا، سنُجري بحثًا غير حساس لحالة الأحرف عن ‘ClickHouse’ OR ‘clickhouse’. القراءة المباشرة معطّلة (المسح القياسي)
تم تفعيل القراءة المباشرة (قراءة سريعة للفهرس)
بدمج النتائج المستخرجة من الفهرس، يصبح استعلام القراءة المباشرة أسرع بمقدار 34 مرة (0.450s مقابل 0.013s)، كما يتجنب قراءة 9.58 GB من بيانات الأعمدة. في هذه الحالة تحديدًا، ستكون الصياغة hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) هي المفضلة والأكثر كفاءة.

ضبط فهرس النص

حاليًا، تتوفر ذاكرات تخزين مؤقت لكتل القاموس بعد إلغاء التسلسل، وللرؤوس، ولقوائم الترحيل الخاصة بفهرس النص لتقليل عمليات الإدخال/الإخراج. يمكن تمكينها عبر الإعدادات use_text_index_dictionary_cache وuse_text_index_header_cache وuse_text_index_postings_cache على التوالي. وهي معطلة افتراضيًا. راجع إعدادات الخادم التالية لتهيئة ذاكرة التخزين المؤقت.

إعدادات الخادم

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

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

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

آخر تعديل في ٢٩ يونيو ٢٠٢٦