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

الوحدة 1 — لماذا محرّك بحث وقاعدة رسوم بيانيّة؟ تثبيت حقيبة Veille

في Veille، الشّركة النّاشئة الصّغيرة من مونتريال المتخصّصة في الرّصد الإعلاميّ، وضعت Inès قاعدة اليوم الأوّل: « قبل أن نكتب أيّ سطر شيفرة، يجب أن نفهم لماذا تخلّى عنّا PostgreSQL في النّسخة الأولى عند خمسين ألف مقال ». Sami وصل للتّوّ؛ هذه الوحدة تُريه لماذا يُكتب بقيّة المنتج بواسطة Elasticsearch وNeo4j، ثمّ تُثبّت حقيبة Docker التي ستُرافق الوحدات السّتّ عشرة.

لماذا لم تعد قاعدة علائقيّة كافية

النّسخة الأولى من محرّك Veille كانت تبحث في المقالات بـSELECT * FROM articles WHERE headline LIKE '%climate change%'. ينجح ذلك حتّى بضعة آلاف من السّطور. بعد خمسين ألفًا، تظهر ثلاث مشكلات في الوقت نفسه.

  • لا ملاءمة. يُعيد لك SQL كلّ السّطور المطابقة بترتيب القرص. « Climate Change Is Real » و« How I Learned to Change My Diet in a Changing Climate » تظهران في المرتبة نفسها. لا يعرف المستخدم ما يقرأ أوّلًا.
  • لا تسامح. LIKE '%climat change%' يُعيد صفر نتيجة. خطأ إملائيّ واحد ويختفي المقال. المفرد والجمع والأحرف الكبيرة والحركات والمشتقّات (« climate »، « climatic »، « climates ») يتطلّب كلّ منها شرطًا خاصًّا به.
  • بطيء، بطيء جدًّا. لا يستطيع LIKE '%…%' استعمال فهرس B-tree: يُعيد PostgreSQL قراءة كامل الجدول عند كلّ بحث. على 200 ألف مقال، يُكلّف كلّ استعلام عدّة ثوانٍ.
المثال المضادّ المفيد

امتدادات النّصّ الكامل في PostgreSQL (tsvector، pg_trgm) تحلّ جزءًا من المشكلة وتكفي أحيانًا. الدّورة ليست ضدّها: هي فقط تُبيّن أنّه حين يصير البحث هو المنتج — ملاءمة، وإكمال تلقائيّ، وagrégations، ولوحات، وتسامح مع الأخطاء، وتعدّد لغات — يُكلّف محرّك مخصَّص أقلّ من تكديس الامتدادات على المدى الطّويل.

الفكرة وراء محرّك البحث: index inversé

Elasticsearch لا يبحث أبدًا في المقالات. بل يبحث في جدول يذكر، لكلّ مصطلح في الكوربوس، المستندات التي تحتويه. يُسمّى هذا index inversé.

على عناويننا، يقطع خطّ التّحليل « Climate Change Is Here » إلى tokens [climate, change, is, here]، ويرمي الكلمات الفارغة (is, here)، ويُحوّل إلى حروف صغيرة، ويطبّق stemmer إنجليزيًّا (changingchange) ثمّ يُسجّل:

climate  → docs [42, 137, 501, 1729, ...]
change → docs [42, 501, 833, 1729, ...]

البحث عن « climate change » يصير عندئذٍ تقاطع قائمتَين مرتَّبتَين: عمليّة فوريّة، مستقلّة عن حجم الكوربوس. يُضيف المحرّك فوق ذلك سكور ملاءمة (BM25) يُصعّد المستندات التي تكون فيها المصطلحات أندر وأكثر تكرارًا داخل المستند — ومن هنا يظهر « Climate Change Is Real » قبل « How I Learned to Change My Diet ».

كما أنّ index inversé هو ما يُتيح التّسامح مع الأخطاء والإكمال التّلقائيّ والتّحليل بالفئات ولوحات Kibana. تفكّك الوحدة 4 خطّ التّحليل؛ وتستغلّ الوحدتان 5 و6 الفهرس من ناحية الاستعلامات.

متى يتفوّق الرّسم البيانيّ على الجملة الشّرطيّة JOIN

القاعدة العلائقيّة ممتازة لإحصاء المقالات حسب الفئة. لكنّها تصير مُثقلة حين نُريد تتبّع علاقات على عدّة قفزات: « أيّ مؤلّفين كتبوا في المواضيع نفسها التي كتب عنها مؤلّف معيّن؟ »، « ما أقصر سلسلة بين موضوعَين عبر المقالات المشتركة؟ »، « أيّ مقال نُوصي به قارئًا أعجبه هذا المقال؟ ». كلّ سؤال يستلزم JOIN إضافيًّا، وكلّ JOIN يُضاعف السّطور الوسيطة.

قاعدة الرّسوم البيانيّة تُخزّن العلاقات مباشرة كعناصر من الدّرجة الأولى. (:Article)-[:ECRIT_PAR]->(:Auteur)-[:ECRIT_PAR]-(:Article)-[:PUBLIE_DANS]->(:Categorie) يُجتاز بمتابعة المؤشّرات، دون تجسيد جدول وسيط. مسار بطول ثلاث قفزات يبقى فوريًّا حتّى على رسم بيانيّ يضمّ مئتَي ألف مقال وثلاثة وعشرين ألف مؤلّف وواحد وأربعين فئة — وهذا ما تُبرهنه الوحدة 12 مباشرة على كوربوس News.

حقيبة Veille في لمحة

كلّ الدّورة تسع في مجلّد واحد، ملفّ docker-compose.yml وسكربتَين: lab.sh لـmacOS وLinux وWSL2 وGit Bash، وlab.ps1 لـWindows PowerShell. القاعدة بسيطة: لا نكتب أبدًا pip install، ولا apt، ولا brew. كلّ ما تحتاج إليه — Elasticsearch، وKibana، وNeo4j مع APOC، ومُستوردة الكوربوس، وcontainer Python مع العملاء الرّسميّين، وOpenSearch للمقارنة في الوحدة 9 — يعمل داخل Docker.

تحميل المجلّد وفتحه

حمّل الحقيبة (42-elasticsearch-neo4j.zip، 30 كيلوبايت)، وفكّ ضغطها، وافتح طرفيّة داخلها. على macOS وLinux وWSL2، اجعل السّكربتات قابلة للتّنفيذ مرّة واحدة بـchmod +x lab.sh doctor.sh. يبدو المجلّد كالآتي:

42-elasticsearch-neo4j/
docker-compose.yml
env.example
lab.sh lab.ps1
doctor.sh doctor.ps1
elasticsearch/mappings/news.json
importer/import_news.py
neo4j/cypher/ neo4j/import/
python/ data/
Windows بدون WSL2

lab.ps1 يُعيد إنتاج lab.sh بالضّبط داخل PowerShell. كلّ أوامر الدّورة ستُعطى بصيغة ./lab.sh <sous-commande>؛ ومكافئ Windows هو .\lab.ps1 <sous-commande>. سنُذكّر بصيغة PowerShell مرّة واحدة لكلّ وحدة.

التّحقّق من الجهاز بـdoctor

الأمر الأوّل الذي يُشغَّل هو التّشخيص. يتحقّق من Docker، وDocker Compose v2، والذّاكرة المخصَّصة، والمنافذ 9200 و5601 و7474 و7687، وvm.max_map_count، والمساحة على القرص، والوصول إلى سجلّ الصّور docker.elastic.co.

./lab.sh doctor            # macOS, Linux, WSL2, Git Bash
.\lab.ps1 doctor # Windows PowerShell

تنفيذ ناجح يبدو كالآتي:

Kit Veille — diagnostic

[OK] Docker 29.0.2 — démon joignable
[OK] Docker Compose v2.36.1
[OK] mémoire allouée à Docker : 8 Go
[OK] port 9200 libre
[OK] port 5601 libre
[OK] port 7474 libre
[OK] port 7687 libre
[OK] vm.max_map_count = 262144
[OK] espace disque libre : 42 Go
[OK] docker.elastic.co joignable (le premier ./lab.sh up téléchargera ≈ 3 Go)

Prêt. Lancez : ./lab.sh up

مثال لفشل نموذجيّ مع تصحيحه:

[KO] port 9200 déjà occupé par un autre programme
→ Linux/macOS : sudo lsof -iTCP:9200 -sTCP:LISTEN
Windows : netstat -ano | findstr :9200
— arrêtez ce programme (souvent un ancien conteneur : docker ps)
[KO] mémoire allouée à Docker : 3 Go — insuffisant (4 Go minimum, 6 Go recommandés)
→ Docker Desktop → Settings → Resources → Memory
WSL2 : %UserProfile%\.wslconfig → [wsl2] memory=8GB, puis wsl --shutdown

يتبع كلّ [KO] بالأمر الدّقيق الذي يُصحّحه. لا ينبغي أن تبقى محجوبًا أكثر من ثلاثين ثانية عند متطلّب مسبق: هذا هو أوّل تعهّد للدّورة.

Windows وWSL2

Docker Desktop يضبط vm.max_map_count تلقائيًّا في توزيعة WSL2 الدّاخليّة الخاصّة به. إذا كنت قد ثبّتَ Docker Engine في توزيعة WSL2 مستقلّة، سيطلب doctor منك تصديره يدويًّا.

تشغيل المنصّة بـup

./lab.sh up

يُشغّل الأمر elasticsearch، وينتظر أن يصير healthy، ثمّ يُطلق الخدمة المؤقّتة setup التي تُعرّف كلمة سرّ kibana_system، ثمّ يُشغّل kibana وNeo4j. يستغرق التّشغيل الأوّل حوالي أربع دقائق: تحميل الصّور (Elasticsearch 9.5.3 يزن أكثر بقليل من غيغابايت، وNeo4j 5.26 حوالي ستّمئة ميغابايت). التّشغيلات اللّاحقة تدور في دقيقتَين وأربعين ثانية على جهاز عاديّ.

في النّهاية، يعرض المخرج:

NAME              STATUS                    PORTS
veille-es Up 2 minutes (healthy) 0.0.0.0:9200->9200/tcp
veille-kibana Up 1 minute (healthy) 0.0.0.0:5601->5601/tcp
veille-neo4j Up 2 minutes (healthy) 0.0.0.0:7474->7474/tcp, 0.0.0.0:7687->7687/tcp

Accès
Elasticsearch http://localhost:9200 (elastic / veille2026)
Kibana http://localhost:5601 (elastic / veille2026)
Neo4j Browser http://localhost:7474 (neo4j / veille2026) bolt://localhost:7687

الاتّصال بـKibana وبـNeo4j Browser

افتح http://localhost:5601 في متصفّح. المستخدم هو elastic وكلمة السّرّ veille2026. يفتح Kibana بالفرنسيّة، ويأخذ حوالي عشر ثوانٍ لتحميل صفحته الأولى. اذهب إلى Management → Dev Tools: هذه هي وحدة التّحكّم التي سنستعملها لكلّ استعلامات الدّورة.

اكتب هذا الاستعلام الأوّل في Dev Tools وانقر على السّهم الأخضر (اختصار Ctrl-Entrée):

GET /

ينبغي أن تُشاهد ردّ عقدة Elasticsearch باسمها وإصدارها واسم cluster ورسالة ترحيب. هذا هو أوّل نداء ناجح لك على الـAPI.

افتح الآن http://localhost:7474. يطلب منك Neo4j Browser عنوان URI: اترك bolt://localhost:7687، والمستخدم neo4j، وكلمة السّرّ veille2026. بعد الاتّصال، اكتب في الشّريط العلويّ:

CALL dbms.components() YIELD name, versions, edition

تُشير النّتيجة إلى Neo4j Kernel، الإصدار 5.26.x، ونسخة community. الرّسم البيانيّ فارغ في الوقت الحاليّ؛ ستملؤه الوحدة 10.

استعلام Elasticsearch بدون Kibana

للتّحقّقات السّريعة عبر سطر الأوامر، يُنفّذ ./lab.sh es <chemin> طلب GET موثَّقًا:

./lab.sh es _cat/indices?v
./lab.sh es _cluster/health?pretty
./lab.sh es _cat/nodes?v

هذا الأمر محفوظ لطلبات GET بلا جسم. كلّ العمليّات الأخرى (POST، PUT، DELETE، _search مع جسم JSON) تمرّ عبر وحدة التّحكّم Dev Tools في Kibana. بذلك تتجنّب فخّ علامات الاقتباس المهرَّبة في الطّرفيّة.

الإيقاف وإعادة التّشغيل

./lab.sh down          # arrête tout, conserve les données
./lab.sh reset # arrête tout ET supprime les données (retour à zéro)
./lab.sh status # état, URL et identifiants
./lab.sh logs kibana # suivre les journaux d'un service

down هو الحركة المعتادة في نهاية اليوم: تحتفظ أحجام Docker بالبيانات، ويُعاد up التّالي في ثلاث دقائق. reset هو حركة الطّوارئ: تُحذف الحاويات، وتُمحى الأحجام، ويُحذف neo4j/import/news.csv، ويعود up التّالي من حالة جديدة كلّيًّا. الملفّ المحمَّل data/News_Category_Dataset_v2.json يُحفَظ لتجنّب تحميل ثمانين ميغابايت من جديد.

حاوية واحدة، دور واحد

تُشغّل الحقيبة ستّ حاويات رئيسيّة. فهم من يفعل ماذا يجعل السّجلّات قابلة للقراءة.

الحاويةالدّور
veille-esعقدة Elasticsearch 9.5.3: cluster بعقدة واحدة، الأمن مفعَّل، HTTP بلا TLS، heap مضبوط على 1 غيغابايت.
veille-setupخدمة مؤقّتة تُعرّف بعد veille-es كلمة السّرّ الدّاخليّة لـkibana_system ثمّ تنتهي.
veille-kibanaKibana 9.5.3 بالفرنسيّة، مربوطة بـveille-es، مع Dev Tools وDiscover وLens واللّوحات.
veille-neo4jNeo4j 5.26 community مع الملحق APOC مُثبَّتًا مسبقًا؛ يُثبِّت neo4j/import وneo4j/cypher.
veille-importerحاوية ملف tools، عند الطّلب: تُحمّل الكوربوس وتُنشئ index news وتفهرس بـ_bulk.
veille-pythonحاوية ملف tools: Python 3 مع العملاء الرّسميّين elasticsearch وneo4j مثبَّتَين مسبقًا.

حاوية سابعة وثامنة، veille-opensearch وveille-os-dashboards، تنامان تحت ملف opensearch. تنطلقان فقط حين تُشغّل ./lab.sh opensearch-up في الوحدة 9، على المنفذَين 9201 و5602 حتّى لا يحدث تعارض مع Elasticsearch وKibana.

ما تفعله الحقيبة عوضًا عنك

تُسوّي الحقيبة بصمت كلّ ما أضاع ساعات على المجموعات السّابقة:

  • كلمة سرّ kibana_system تُعرَّف بواسطة الخدمة setup عبر الـAPI /_security/user/kibana_system/_password. لن تضطرّ أبدًا لنسخ token يدويًّا بين Elasticsearch وKibana.
  • heap Java (-Xms1g -Xmx1g) مكتوب في docker-compose.yml، لا في .env. فخّ علامات الاقتباس التي تُوهم بأنّ heap لم يُطبَّق يختفي.
  • عتبة القرص مُعطَّلة (cluster.routing.allocation.disk.threshold_enabled=false): قرص بنسبة امتلاء خمسة وتسعين بالمئة لن يُحوِّل فهرسك إلى وضع القراءة فقط أثناء الورشة.
  • healthchecks تنتظر أن يستجيب كلّ خدمة فعلًا قبل أن يُعيد ./lab.sh up اليد. لن تفتح Kibana أبدًا وElasticsearch لا يزال في طور التّشغيل.
  • APOC مضاف إلى Neo4j عبر NEO4J_PLUGINS=["apoc"] ومحفوظ في حجم neo4j-plugins: تحميل مرّة واحدة، ثمّ يبقى متاحًا حتّى دون اتّصال.
  • مفتاح تشفير Kibana بأكثر من اثنين وثلاثين محرفًا مُقدَّم في env.example لتفادي السّطر الأحمر « Kibana requires a value for xpack.encryptedSavedObjects.encryptionKey ».
تعديل env.example

يُنسخ الملفّ env.example إلى .env عند أوّل تشغيل. يمكن تغيير كلمات السّرّ (حروف وأرقام فقط لتجنّب مشاكل التّهريب في الشّيل) أو تحديد NEWS_LIMIT=20000 لاستيراد سريع على جهاز صغير. بعد تعديل .env، يجب ./lab.sh reset ثمّ ./lab.sh up: كلمات السّرّ تُكتَب في الحجم عند أوّل تشغيل.

متطلّبات الجهاز

أربعة غيغابايتات من RAM مخصَّصة لـDocker تكفي لـElasticsearch وKibana وNeo4j. سنّة غيغابايت ضروريّة لإضافة OpenSearch بالتّوازي في الوحدة 9. تحتاج إلى حوالي عشرة غيغابايت من المساحة على القرص (ثلاثة للصّور، اثنان للبيانات)، ومنفذ خارج نحو docker.elastic.co وregistry-1.docker.io عند التّشغيل الأوّل. أيّ من هذه القيود لا يُتحقّق منها يدويًّا: ./lab.sh doctor يفعل ذلك عوضًا عنك.

جرّب 1 — التّشخيص والتّشغيل

شغّل ./lab.sh doctor، وصحّح كلّ [KO] باتّباع السّهم، ثمّ شغّل ./lab.sh up. سجّل، في ملفّ journal.md، الوقت الذي استغرقه أوّل up عندك.

الحلّ

على جهاز عاديّ مع Docker Desktop، يُعيد doctor اليد في ثلاث ثوانٍ. أوّل up يستغرق بين ثلاث وخمس دقائق (بما فيه تحميل الصّور)؛ والتّشغيلات اللّاحقة تدور في دقيقتَين وأربعين ثانية. إذا تجاوز up عندك ثماني دقائق، افتح طرفيّة أخرى وشغّل ./lab.sh logs elasticsearch لرؤية ما يجري.

جرّب 2 — تحقّق ثلاث مرّات

في Kibana Dev Tools، نفّذ:

GET /
GET /_cluster/health
GET /_cat/nodes?v

سجّل اسم cluster، ولونه (green أو yellow)، واسم العقدة. ثمّ في طرفيّة، شغّل الاستعلام الأخير نفسه بـ./lab.sh es _cat/nodes?v. تحقّق أنّ المخرج مطابق.

الحلّ

اسم cluster هو veille، ولونه green (سنرى لماذا في الوحدة 2)، واسم العقدة veille-es. الأمر ./lab.sh es _cat/nodes?v يُعيد المخرج الجدوليّ نفسه بالضّبط: هي واجهة HTTP نفسها المستدعاة بالمعرِّفات نفسها، مرّة عبر Kibana ومرّة عبر curl داخل حاوية veille-es.

جرّب 3 — أطفئ بسلاسة، أعد التّشغيل

شغّل ./lab.sh down، وانتظر حتّى تختفي الحاويات الثّلاث (docker ps)، ثمّ أعد ./lab.sh up. اقتنص وقت التّشغيل الثّاني. أخيرًا، جرّب ./lab.sh reset — تختفي الأحجام — ثمّ أعد ./lab.sh up: سترى الفرق.

الحلّ

بعد down ثمّ up، ينزل التّشغيل إلى حوالي دقيقتَين وأربعين ثانية: الصّور في الذّاكرة المؤقّتة، وheap مُهيَّأ سلفًا، وElasticsearch يرتفع أسرع. بعد reset ثمّ up، يرتفع الوقت إلى ثلاث أو أربع دقائق لأنّه يجب إعادة تعريف كلمة سرّ kibana_system ولأنّ Kibana يُعيد إنشاء فهارسه الدّاخليّة. هذا طبيعيّ.

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

  • LIKE '%…%' في SQL ليس محرّك بحث: لا ملاءمة، ولا تسامح، وبطيء بعد بضعة آلاف من السّطور.
  • يبحث Elasticsearch عبر index inversé: « أيّ مستندات تحتوي هذا المصطلح » بدل « أيّ مصطلحات يحتوي هذا المستند ».
  • قاعدة رسوم بيانيّة تجعل مسارات العمق المتغيّر فوريّةً، وهي المسارات التي تدفع قاعدة علائقيّة ثمنها في JOIN متكرّرة.
  • تسع حقيبة Veille في مجلّد واحد وتُقاد بثلاثة أوامر: doctor وup وimport-news.
  • ./lab.sh doctor يُشخّص الجهاز ويُعطي أمر التّصحيح الدّقيق لكلّ مشكلة.
  • Kibana Dev Tools (http://localhost:5601، elastic / veille2026) هو وحدة تحكّم الدّورة المرجعيّة؛ ./lab.sh es <chemin> يُغطّي طلبات GET عبر سطر الأوامر.
  • down يحفظ البيانات، reset يعيد من الصّفر: هاتان حركتاك في نهاية اليوم.

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

  • ./lab.sh up يبقى معلَّقًا على « attente de Kibana » → يأخذ Kibana أحيانًا أكثر من ثلاث دقائق في التّشغيل الأوّل على قرص بطيء. تحقّق بـ./lab.sh logs kibana: إن رأيت Kibana is now available، اصبر؛ وإلّا، اقرأ الرّسالة الحمراء.
  • [KO] port 9200 déjà occupé par un autre programme → حاوية Elasticsearch قديمة لا تزال تعمل. docker ps لتحديدها، ثمّ docker stop <nom> لإيقافها، ثمّ ./lab.sh doctor.
  • Kibana يعرض « Kibana server is not ready yet » → الخدمة setup لم تنتهِ. انتظر ثلاثين ثانية أو راجع ./lab.sh logs setup؛ كخيار أخير، ./lab.sh reset ثمّ ./lab.sh up.
  • vm.max_map_count منخفض جدًّا على Linux أصليّsudo sysctl -w vm.max_map_count=262144 يُصحّح الجلسة الجارية؛ لجعله دائمًا، يُعطيك ./lab.sh doctor الأمر الذي يجب إضافته في /etc/sysctl.d/99-elasticsearch.conf.

للاستزادة