انتقل إلى المحتوى الرئيسي

الوحدة 2 — المفاهيم الأساسيّة لـElasticsearch: cluster وnœud وindex وshard وdocument

الحقيبة تعمل، وKibana يستجيب. قبل فهرسة مقال واحد، تريد Inès أن يعرف Sami قراءة cluster: كم عقدة، وكم فهرسًا، وكم shard، وأيّ لون، ولماذا. هذه الوحدة تُقدّم القاموس الأدنى الذي يجعل نصف رسائل الخطأ في بقيّة الدّورة بديهيًّا.

القاموس، بترتيب لقائنا به

يعرض Elasticsearch مفاهيمه بتداخلات متتالية. نأخذها من الأعلى نحو الأسفل.

Cluster

Cluster هو مجموعة من خادم واحد أو أكثر يُعلنون انتماءهم إلى المجموعة نفسها. يُسمّى cluster لدينا veille — تقرؤه في docker-compose.yml تحت cluster.name=veille. لكلّ cluster اسم، وصحّة (green أو yellow أو red)، وإصدار، وهو يمتلك جماعيًّا كلّ البيانات.

Nœud

Nœud هو عمليّة Elasticsearch تعمل على خادم انضمّت إلى cluster. تُشغّل الحقيبة عقدة وحيدة، veille-es، في وضع discovery.type=single-node. في الإنتاج، cluster من ثلاث عقد أو أكثر هو القاعدة، لكنّ عقدة وحيدة تكفي تمامًا لاستكشاف المفاهيم ومعالجة 200 853 مقالًا من الكوربوس.

Index

Index هو التّجميع المنطقيّ للمستندات من نوع واحد — مقالات News لدينا تُشكّل الفهرس news. يعرض كلّ فهرس كتلتَين من الإعداد: settings (عدد shards، وعدد replicas، والمحلّلات) وmapping (الحقول ونوعها وخياراتها). ثمّ يعيش الفهرس في shard واحد أو أكثر ماديًّا.

Shard وréplique

Shard هو تقسيم Lucene لفهرس. تُوزَّع مستندات الفهرس بين shards بحساب هاش _id. كلّ shard هو محرّك Lucene كامل ومستقلّ، قادر على استيعاب عدد كبير من المستندات وخدمة الاستعلامات.

Réplique هي نسخة من shard رئيسيّ، مُوضَعة على عقدة أخرى. تخدم غرضَين: تحمّل فقدان عقدة (تَوفّر عالٍ)، واستيعاب حركة القراءة. صفر réplique لا معنى له إلّا في ورشة — عقدة وحيدة لا يمكنها على أيّ حال أن تستضيف نسخة عن shard نفسها. هذه هي حالة الحقيبة.

Document

Document هو كائن JSON مُخزَّن في فهرس. له معرِّف _id (تُقدّمه أنت أو يُولَّد)، و_source (الـJSON كما أرسلتَه)، وبيانات وصفيّة للإصدار (_seq_no، _primary_term). مقال News هو مستند.

المقابلة الذّهنيّة مع العالم العلائقيّ

يُساعد الجدول التّالي، شرط ألّا نتمسّك به: Elasticsearch ليس قاعدة علائقيّة وهذه المكافئات تقريبيّة.

Elasticsearchالعلائقيّ (تقريبيّ)
Clusterخادم SGBD
Nœudمثيل
Indexجدول
Shardتقسيم لجدول
Documentسطر
حقلعمود
_idمفتاح رئيسيّ
Mappingمخطّط (CREATE TABLE)
لا JOIN

لا يوجد مكافئ في Elasticsearch لـJOIN. الحقل nested (لمحة في الوحدة 4) يسمح بالكائنات المتضمَّنة، لكنّنا لا نربط فهرسَين بمفتاح خارجيّ. هذا خيار: يجب أن يستطيع كلّ shard الرّدّ لوحده حتّى يبقى سريعًا. إن كنت تحتاج إلى علاقات، فذلك من نصيب Neo4j (الوحدات 10 إلى 12).

النّظر إلى cluster

افتح Kibana Dev Tools (Management → Dev Tools). كلّ الاستعلامات أدناه تُلصَق مباشرة في وحدة التّحكّم اليسرى؛ اختصار Ctrl-Entrée (Cmd-Entrée على macOS) يُنفّذها.

GET / — العقدة تُقدّم نفسها

GET /

تحصل على اسم العقدة (veille-es)، واسم cluster (veille)، وإصدار Elasticsearch (9.5.3)، وإصدار Lucene، ومعرِّف UUID الخاصّ بـcluster. هذه هي أقصر نسخة من « أنا حيّ، وأستجيب، وها أنا ».

GET /_cluster/health — النّبض

GET /_cluster/health

مخرج نموذجيّ على حقيبتنا:

{
"cluster_name": "veille",
"status": "green",
"number_of_nodes": 1,
"number_of_data_nodes": 1,
"active_primary_shards": 1,
"active_shards": 1,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 0
}

يأخذ الحقل status ثلاث قيم:

  • green: كلّ shards الرّئيسيّة وكلّ replicas مُسنَدة.
  • yellow: كلّ الرّئيسيّة مُسنَدة، لكنّ ريبليكا واحدة على الأقلّ ناقصة.
  • red: shard رئيسيّ واحد على الأقلّ غير مُسنَد — مستندات لا يمكن الوصول إليها.

الحقيبة green لأنّ الفهرس news أُنشئ بـnumber_of_replicas: 0: لا توجد ريبليكا لوضعها، فلا شيء ناقص. كثير من الدّروس تنطلق بالمعامل الافتراضيّ لريبليكا واحدة وتعرض yellow — فيتساءل Sami حينها عمّا كسره. الجواب: لا شيء، فالعقدة الوحيدة لا يمكنها ببساطة استضافة نسخة عن shard الرّئيسيّ الخاصّ بها.

GET /_cat/nodes?v — مَن يُشكّل cluster

./lab.sh es _cat/nodes?v

أو في Dev Tools:

GET /_cat/nodes?v

ترى عقدة وحيدة، veille-es، وعنوان IP الدّاخليّ لها، ونسبة الذّاكرة وheap المستهلَكَة، وحملها ودورها (cdfhilmrstw: العقدة تفعل كلّ شيء في الوقت نفسه — طبيعيّ في single-node).

GET /_cat/indices?v — أيّ فهارس موجودة

GET /_cat/indices?v

قبل الاستيراد، لا يعرض المخرج سوى فهارس Kibana النّظاميّة (.kibana_*، .security-*) المسبوقة بنقطة. بعد ./lab.sh import-news، يظهر سطر:

health status index    uuid       pri rep docs.count docs.deleted store.size pri.store.size
green open news ... 1 0 200853 0 ... ...

القراءة: shard رئيسيّ واحد (pri: 1)، صفر ريبليكا (rep: 0)، 200 853 مستندًا، صفر محذوف، ووزن يقارب مئة وأربعين ميغابايت.

GET /_cat/shards?v — أين تعيش shards

GET /_cat/shards/news?v
index shard prirep state   docs   store ip         node
news 0 p STARTED 200853 ... 172.20.0.3 veille-es

shard رئيسيّ وحيد (p)، مُشغَّل، يستضيف الـ200 853 مستندًا. prirep = r يعني ريبليكا.

إنشاء فهرس ووصفه وحذفه يدويًّا

قبل فهرسة News بواسطة الحقيبة، لنُولّد ونُتلف فهرسًا صغيرًا للعرض: Léa تريد تتبّع عمليّات بحثها اليدويّة في فهرس مستقلّ.

الإنشاء مع settings وmapping

PUT recherches_lea
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"sujet": { "type": "keyword" },
"requete": { "type": "text" },
"date": { "type": "date", "format": "yyyy-MM-dd" },
"resultats": { "type": "integer" }
}
}
}

الردّ المتوقَّع:

{ "acknowledged": true, "shards_acknowledged": true, "index": "recherches_lea" }

لاحظ ما ستُفكّكه الوحدة 4: sujet هو keyword (قيم تُقارَن بدقّة، وقابلة للاستعمال في agrégation)، وrequete هو text (مُحلَّل للبحث)، وdate بصيغة صريحة لتجنّب أن يتخبّط mapping الدّيناميكيّ في التّخمين.

API 9.x، بلا _doc في العنوان

منذ Elasticsearch 7، اختفت الأنواع المسمّاة. نكتب PUT /recherches_lea مع mapping في الجذر؛ الدّروس القديمة بـPUT /recherches_lea/_doc/_mapping أو include_type_name=true صارت مهجورة ومرفوضة في 9.x.

الوصف

GET recherches_lea

يُعيد settings (مع قيم افتراضيّة يُضيفها Elasticsearch: creation_date، uuid، version)، وmapping كما هو مُسجَّل، وaliases (لا شيء هنا). للحصول على الجزء الخاصّ بـmapping فقط:

GET recherches_lea/_mapping

تعديل setting قابل للإعادة

بعض settings ديناميكيّ (قابل للتّعديل أثناء التّشغيل): number_of_replicas، refresh_interval. وأخرى ثابتة (تُثبَّت عند الإنشاء): number_of_shards. الانتقال من shard واحد إلى shard اثنين يفرض إعادة فهرسة.

PUT recherches_lea/_settings
{ "index": { "refresh_interval": "5s" } }

ستستغلّ الوحدة 3 هذه الرّافعة أثناء الاستيراد: نضبط refresh_interval على 30s أثناء تحميل 200 853 مستندًا، ثمّ نُعيده إلى 1s. مضاعفة معدّل الفهرسة بشكل ملموس.

الحذف

DELETE recherches_lea

الردّ {"acknowledged": true}. يُفكَّك shard وتُمحى ملفّات Lucene الخاصّة به ويختفي الفهرس من _cat/indices.

عمليّات الحذف

DELETE لا رجعة عنه من جانب cluster: لا سلّة، ولا rollback. على cluster إنتاج، من الحكمة تفعيل action.destructive_requires_name=true لمنع DELETE _all أو DELETE *. تبقى الحقيبة متسامحة حتّى لا تعرقل التّعلّم.

ما يُغيّره shard، وما تُغيّره الرّيبليكا

معاملان اثنان، وأثران يجب عدم الخلط بينهما.

  • shards رئيسيّة أكثر = يمكن توزيع الفهرس نفسه على عقد أكثر، ويستقبل كلّ shard مستندات أقلّ، وتتوازى الفهرسة والبحث بشكل أوسع. الكلفة: كلّ shard يستهلك ذاكرة (buffers، وبُنى Lucene) وينسّق استعلاماته. القاعدة التّجريبيّة التي نشرتها Elastic هي إبقاء كلّ shard بين عشرة وخمسين غيغابايت، وعدم تجاوز نحو عشرين shard لكلّ غيغابايت من heap.
  • replicas أكثر = مرونة أكبر وقدرة قراءة أعلى، دون فائدة للفهرسة (بل بالعكس: كلّ كتابة تُنسَخ). لا معنى لريبليكا إلّا على عقدة غير العقدة الرّئيسيّة؛ ولريبليكتَين فائدة تبدأ من ثلاث عقد.

على الـ200 853 مستندًا في News، يكفي shard وحيد تمامًا (إجماليّ التّخزين نحو مئة وأربعين ميغابايت). في الإنتاج على مليارات المستندات، نُقسّم إلى عشرات shards المُوزَّعة على عدّة data nodes.

Heap Java والقرص، في صفحة واحدة

يعمل Elasticsearch على JVM. توتّران يُحدّدانه:

  • heap Java (-Xms1g -Xmx1g في docker-compose.yml عندنا): نصف RAM الخاصّة بالحاوية، لا أكثر من واحد وثلاثين غيغابايت في الإنتاج (بعد ذلك، تُغيّر JVM وضع مؤشّراتها وتفقد الكفاءة). تقرأ heap المستهلَك في GET /_cat/nodes?v&h=name,heap.percent.
  • القرص: يراقب Elasticsearch المساحة الحرّة، وافتراضيًّا يُحوّل الفهارس تلقائيًّا إلى وضع القراءة فقط عند بلوغ عتبة (low، high، flood_stage). تُعطّل الحقيبة هذا السّلوك (cluster.routing.allocation.disk.threshold_enabled=false) لعدم عرقلة الورشة. في الإنتاج، نُبقي هذه الآليّة ونُضيف مساحة.

أمر مفيد لمراقبة الاثنَين معًا:

GET /_cat/nodes?v&h=name,heap.percent,ram.percent,disk.used_percent

جرّب 1 — قراءة cluster

في Kibana Dev Tools، نفّذ GET /_cluster/health. سجّل status وnumber_of_nodes وactive_primary_shards وactive_shards وunassigned_shards. ثمّ نفّذ GET /_cat/indices?v. كم فهرسًا موجودًا؟ كم منها ليس فهارس نظاميّة (المسبوقة بنقطة)؟

الحلّ

الحالة green، عقدة واحدة، shard رئيسيّ أو أكثر مُسنَد حسب الفهارس النّظاميّة التي أنشأها Kibana مسبقًا (.kibana_*، .security-*، .apm-* — القائمة تتطوّر). صفر shard غير مُسنَد. قبل الاستيراد، لا فهرس مستخدم: فقط الفهارس النّظاميّة. بعد ./lab.sh import-news، يُضاف فهرس مستخدم: news.

جرّب 2 — إنشاء فهرس صغير وحذفه

أنشئ فهرسًا notes_veille بـshard واحد وصفر ريبليكا وحقلَين (titre من نوع text، tag من نوع keyword). تحقّق من ظهوره في _cat/indices، وصفه، ثمّ احذفه.

الحلّ
PUT notes_veille
{
"settings": { "number_of_shards": 1, "number_of_replicas": 0 },
"mappings": {
"properties": {
"titre": { "type": "text" },
"tag": { "type": "keyword" }
}
}
}
GET _cat/indices/notes_veille?v
GET notes_veille
DELETE notes_veille

تحقّق من أنّ استعلام GET _cat/indices/notes_veille?v الثّاني يُعيد الخطأ index_not_found_exception: الحذف صار نافذًا.

جرّب 3 — محاكاة yellow والعودة إلى green

أنشئ فهرسًا بريبليكا واحدة ولاحظ ما يصير عليه في cluster ذي عقدة واحدة. ثمّ أعد صفر ريبليكا وتحقّق من جديد.

PUT test_yellow
{ "settings": { "number_of_shards": 1, "number_of_replicas": 1 } }
GET _cluster/health/test_yellow

ماذا يقول status؟ عدّل عدد replicas أثناء التّشغيل:

PUT test_yellow/_settings
{ "index": { "number_of_replicas": 0 } }

تحقّق من الصحّة من جديد، ثمّ احذف الفهرس.

الحلّ

أوّل GET /_cluster/health/test_yellow يُعيد "status": "yellow" مع unassigned_shards: 1: الرّيبليكا المطلوبة ليس لها مكان (عقدة واحدة). بعد تحويل number_of_replicas إلى صفر، تعود صحّة هذا الفهرس إلى green فورًا — يُحرّر Elasticsearch انتظار الإسناد.

DELETE test_yellow

احفظ المنطق: yellow على cluster ذي عقدة واحدة ليس عطلًا، بل خيار إعداد. على cluster من ثلاث عقد أو أكثر، yellow مستمرّ يستحقّ تشخيصًا حقيقيًّا (GET _cluster/allocation/explain).

النقاط الأساسيّة

  • Cluster → nœud → index → shard → document: خمسة مستويات بهذا التّرتيب.
  • لكلّ فهرس Elasticsearch settings (shards، replicas، refresh) وmapping (الحقول والأنواع).
  • green = كلّ شيء مُسنَد؛ yellow = shard رئيسيّ موجود لكن ريبليكا ناقصة؛ red = shard رئيسيّ ناقص.
  • الحقيبة green لأنّ الفهرس news مُهيَّأ بصفر ريبليكا — منطقيّ على cluster بعقدة واحدة.
  • shard هو محرّك Lucene مستقلّ؛ نضبط عدده عند الإنشاء، ونستطيع تعديل عدد replicas أثناء التّشغيل.
  • _cat/indices و_cat/nodes و_cat/shards هي أوامر التّشخيص الثّلاثة الأكثر فائدة: احفظها.
  • في 9.x، لم نعد نضع _doc في عنوان mapping، ولا include_type_name، ولا نوع string: هذه الصّيغ مرفوضة.

استكشاف الأخطاء

  • GET /_cluster/health يُعيد 401 في Dev Tools → لم يعد المستخدم elastic معترفًا به؛ كلمة سرّ .env تغيّرت بعد أوّل up. ./lab.sh reset ثمّ ./lab.sh up.
  • GET /_cat/indices?v يبقى فارغًا أو ينتهي بمهلة → لم ينتهِ Elasticsearch من التّشغيل؛ راجع ./lab.sh logs elasticsearch وانتظر [YELLOW] to [GREEN] على الفهارس النّظاميّة.
  • illegal_argument_exception, mapper_parsing_exception عند إنشاء فهرس → مفتاح mapping خطأ إملائيّ (typo على properties) أو نوع غير صالح (string لم يعد موجودًا في 9.x، استبدله بـtext أو keyword).
  • الحالة تصير red بعد import-news → على الأقلّ shard رئيسيّ واحد لم يستطع التّهيئة؛ سيبحث ./lab.sh logs elasticsearch عن رسالة disk usage exceeded flood-stage watermark أو translog corruption، ثمّ ./lab.sh reset.

للاستزادة