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

الوحدة 10 — قواعد الرسوم البيانيّة، وNeo4j، والخطوات الأولى في Cypher

ترسم إيناس على السبّورة سؤالًا تودّ Veille أن تُجيب عنه لعملائها: «من يكتب في الفئات نفسها التي يكتب فيها Lee Moran، وعلى أيّ مسافة كتابيّة؟». في SQL يتحوّل الاستعلام إلى تكديس JOIN متعاقبة. المسألة نفسها، في رسم بيانيّ، تنحصر في ثلاثة أسطر. يُثبِّت سامي Neo4j محلّيًّا عبر الحقيبة؛ تُعلّمه هذه الوحدة المفردات، ولغة Cypher، وتجعله يبني رسمًا بيانيًّا مصغَّرًا للفريق سيكون ميدان تمرينه.

أسماء التصنيفات والعلاقات بالفرنسيّة

تبقى تصنيفات Neo4j (Labels) وأنواع العلاقات كما هي بالفرنسيّة (:Article، :Categorie، :Auteur، :Personne، :Ville، :Competence، :PUBLIE_DANS، :ECRIT_PAR، :HABITE، :MAITRISE، :ENCADRE …) لأنّها مُعرَّفة داخل سكربتات الحقيبة المشتركة (neo4j/cypher/*.cypher). لا تُترجَم هذه الأسماء حتّى تعمل الاستعلامات كما هي.

متى يفوز الرسم البيانيّ على العلائقيّ

القاعدة العلائقيّة ممتازة لتجميع الأعمدة، أقلّ كفاءةً لتتبّع علاقات بعمق متغيّر. ثلاث علامات تدلّ على أنّ المسألة مسألة رسم بيانيّ:

  • يستحضر السؤال مسارًا: «هل هناك رابط بين A وB؟»، «على أيّ مسافة؟».
  • ليس العمق ثابتًا: «من يؤطّر من، على مستوى أو مستويَين؟»، «توصية على قفزتَين».
  • تلعب الكيانات عدّة أدوار: قد يكون المؤلّف مصدرًا أيضًا، وقد تكون الفئة مركز اهتمام أيضًا.

في SQL يترجَم مسار بطول 3 إلى ثلاث عبارات JOIN صريحة. في Cypher نكتب (a)-[*1..3]->(b) ويعثر المحرّك على المسار بنفسه. المكسب ليس مكسب تعبير فقط: يُخزِّن محرّك الرسم البيانيّ العلاقات مؤشِّرات ويعبرها بزمن ثابت، خلافًا للـjointure التي تعتمد على فهرس على المفتاح الأجنبيّ. بعبارة أخرى: كلّما زادت أعماق العبور، اتّسعت الفجوة بين SQL وCypher لصالح هذا الأخير.

نموذج الخصائص

يتبع Neo4j property graph model. أربع لبنات تكفي.

  • العقدة — الكيان. تُمثَّل بين قوسَين: (p).
  • التصنيف (Label) — نوع العقدة، مسبوقًا بـ: : (:Personne)، (:Ville). قد تحمل العقدة عدّة تصنيفات.
  • العلاقة الموسومة والموجَّهة — الرابط، بين معقوفتَين وأسهم: -[:ENCADRE]->. النوع إلزاميّ، والاتّجاه أيضًا (حتّى وإن أمكن قراءته في الاتّجاهَين).
  • الخاصّيّة — زوج مفتاح/قيمة تحمله العقدة أو العلاقة: {nom: 'Inès', anciennete: 6}.

نُجمِّع هذه اللبنات في أنماط برسم ASCII:

(p:Personne)-[:HABITE {depuis: 2018}]->(v:Ville {nom: 'Montréal'})

يُقرأ هذا السطر «توجد عقدة p بتصنيف Personne في علاقة HABITE مع العقدة v بتصنيف Ville واسمها Montréal، وتحمل العلاقة الخاصّيّة depuis بقيمة 2018». السطر نفسه يخدم القراءة (في MATCH) والكتابة (في CREATE أو MERGE). هذا هو الرهان الكبير الأوّل للّغة: مفردات واحدة للاستفسار والبناء.

فتح Neo4j Browser

تعرِض الحقيبة Neo4j Community 5.26 على http://localhost:7474 (HTTP) وعلى bolt://localhost:7687 (البروتوكول الثنائيّ للعملاء). افتح المتصفّح.

  • عنوان الاتّصال: bolt://localhost:7687
  • المستخدم: neo4j
  • كلمة المرور: veille2026

بعد الاتّصال، يقبل شريط الأعلى ثلاث عائلات من المُدخلات. استعلامات Cypher (كلّ ما يبدأ بـMATCH أو CREATE أو MERGE أو RETURN …). أوامر Browser المسبوقة بـ: : :play (دروس مصغّرة مدمجة)، و:schema (قائمة التصنيفات والقيود والفهارس)، و:sysinfo (حالة الخادم، وحجم الـheap، وذاكرة الصفحات)، و:history (سجلّ الأوامر). أخيرًا EXPLAIN وأمّا PROFILE (الوحدة 12) فتُسبقان استعلامًا لعرض خطّة تنفيذه.

التبديل بين الرسم البيانيّ والجدول

بعد RETURN، يقترح لوح النتيجة ثلاث أيقونات: الرسم البيانيّ (تصوّر بصريّ)، والجدول (أعمدة)، والنصّ (JSON خام). الجدول مثاليّ للتحقّق من القيم، والرسم البيانيّ أساسيّ لفهم الطوبولوجيا.

سكربت الحقيبة: 10-premiers-pas.cypher

تُسلّم الحقيبة رسمًا بيانيًّا مصغَّرًا من إحدى عشرة عقدة وأربع عشرة علاقة يصف فريق Veille: أربعة أعضاء يقيمون في ثلاث مدن ويتقنون بضع كفاءات، مع ثلاث علاقات تأطير. ينفَّذ بأمر واحد من shell المضيف.

./lab.sh cypher 10-premiers-pas.cypher

يُضاف الملفّ للقراءة فقط داخل الحاوية veille-neo4j تحت /cypher. لنستعرضه كتلةً كتلةً.

الكتلة 1 — قيود الفرادة

CREATE CONSTRAINT personne_nom  IF NOT EXISTS FOR (p:Personne)   REQUIRE p.nom IS UNIQUE;
CREATE CONSTRAINT ville_nom IF NOT EXISTS FOR (v:Ville) REQUIRE v.nom IS UNIQUE;
CREATE CONSTRAINT competence_nom IF NOT EXISTS FOR (c:Competence) REQUIRE c.nom IS UNIQUE;

ثلاث قيود فرادة على الخاصّيّة nom، واحدة لكلّ تصنيف. أثران. أوّلًا، تضمن الفرادة: سيرفض Neo4j إدراج شخصَين :Personne باسم nom نفسه. ثانيًا، تُنشئ ضمنيًّا فهرسًا على nom: تصبح استعلامات MATCH (p:Personne {nom: 'Inès'}) وصولًا مباشرًا، لا مسحًا. تجعل IF NOT EXISTS الأمر عديم التأثير إن أُعيد — يمكن إعادة تشغيل السكربت بلا خطأ.

صياغة Cypher 5

منذ Neo4j 5، نكتب CREATE CONSTRAINT nom IF NOT EXISTS FOR (n:Label) REQUIRE n.prop IS UNIQUE. الصيغة القديمة CREATE CONSTRAINT ON ... ASSERT صارت مهجورة ولم تعد تعمل في مشروع جديد.

الكتلة 2 — العقد

MERGE (:Personne {nom: 'Inès',  role: 'lead data',     anciennete: 6});
MERGE (:Personne {nom: 'Sami', role: 'data engineer', anciennete: 1});
MERGE (:Personne {nom: 'Léa', role: 'analyste', anciennete: 3});
MERGE (:Personne {nom: 'Karim', role: 'développeur', anciennete: 4});

يُحاول كلّ MERGE أوّلًا MATCH على النمط، وإن لم يوجد نفّذ CREATE. هذا هو نمط الـupsert في Cypher. بفضل قيود الكتلة 1، البحث فوريّ. تتبع المدن الثلاث والكفاءات الأربع النمط نفسه:

MERGE (:Ville {nom: 'Montréal', pays: 'Canada'});
MERGE (:Competence {nom: 'Elasticsearch'});
// وهكذا دواليك

الكتلة 3 — العلاقات

تُوضع العلاقات بعد العقد، بالنمط MATCH ... MERGE (a)-[:REL]->(b):

MATCH (p:Personne {nom: 'Inès'}), (v:Ville {nom: 'Montréal'})
MERGE (p)-[:HABITE {depuis: 2018}]->(v);

يستعيد MATCH العقدتَين (وصول بالفهرس بفضل القيد)، ويقوم MERGE بإنشاء العلاقة إن لم توجد. تحمل الخاصّيّة depuis سنة الإقامة. تحمل علاقات MAITRISE مستوى niveau من 1 إلى 5:

MATCH (p:Personne {nom: 'Inès'}), (c:Competence {nom: 'Elasticsearch'})
MERGE (p)-[:MAITRISE {niveau: 5}]->(c);

أخيرًا، ENCADRE تُجسِّد التأطير:

MATCH (a:Personne {nom: 'Inès'}), (b:Personne {nom: 'Sami'})
MERGE (a)-[:ENCADRE]->(b);

الكتلة 4 — الحصيلة

MATCH (n) WITH count(n) AS noeuds
MATCH ()-[r]->() RETURN noeuds, count(r) AS relations;

النتيجة المنتظرة في نهاية التنفيذ:

noeuds | relations
-------+----------
11 | 14

إحدى عشرة عقدة: 4 أشخاص، و3 مدن، و4 كفاءات. أربع عشرة علاقة: 4 من HABITE، و7 من MAITRISE، و3 من ENCADRE.

لماذا MERGE بدلًا من CREATE

يُنشئ CREATE دون تحقّق. إعادة تشغيل السكربت مرّتَين بـCREATE (:Personne {nom: 'Inès'}) سيُدرج شخصَين مختلفَين لإيناس، بمُعرِّفَين داخليَّين مختلفَين. في مجموعة بيانات كبيرة، يصبح هذا الخطأ غير مرئيّ ومكلفًا.

MERGE يوازي INSERT ... ON CONFLICT DO NOTHING مقرونًا بـSELECT ضمنيّ: إن كان السطر موجودًا نُبقيه، وإن لم يكن أنشأناه. سكربت الحقيبة إذن عديم التأثير التكراريّ: إعادة تشغيله تُنتج الرسم البيانيّ نفسه دومًا.

الأنماط الجزئيّة في MERGE

MERGE (a)-[:HABITE]->(b) يُنشئ العلاقة وكلتا العقدتَين إن لم تكونا موجودتَين. لتفادي اختراع عقدة، نُجري أوّلًا مرتَي MATCH ثمّ MERGE على العلاقة فقط، كما في السكربت. هذا هو النمط الآمن.

الاستعلامات الأولى للقراءة

الرسم البيانيّ جاهز. افتح Browser وأطلق الاستعلامات التالية.

عرض الفريق كلّه:

MATCH (p:Personne) RETURN p.nom, p.role, p.anciennete ORDER BY p.anciennete DESC;
p.nom  | p.role         | p.anciennete
-------+----------------+-------------
Inès | lead data | 6
Karim | développeur | 4
Léa | analyste | 3
Sami | data engineer | 1

من يقيم في Montréal — نمط بعقدتَين مع تصفية على المدينة:

MATCH (p:Personne)-[:HABITE]->(:Ville {nom: 'Montréal'})
RETURN p.nom;

المنتظر: Inès، Sami.

من يتقن Elasticsearch عند مستوى 4 أو أكثر — الفلتر يقع على العلاقة:

MATCH (p:Personne)-[r:MAITRISE]->(:Competence {nom: 'Elasticsearch'})
WHERE r.niveau >= 4
RETURN p.nom, r.niveau ORDER BY r.niveau DESC;

المنتظر: Inès بمستوى 5.

من يؤطّر من — نقرأ العلاقة بالاتّجاه الطبيعيّ:

MATCH (mentor:Personne)-[:ENCADRE]->(mentore:Personne)
RETURN mentor.nom, mentore.nom;

المنتظر: Inès → Sami، وInès → Léa، وKarim → Sami.

سلسلة تأطير على مستويَين — مسار بطول متغيّر من 1 إلى 2:

MATCH chemin = (mentor:Personne)-[:ENCADRE*1..2]->(mentore:Personne)
RETURN mentor.nom, mentore.nom, length(chemin) AS profondeur
ORDER BY profondeur, mentor.nom;

هنا يبقى العمق 1 (لا مُؤطَّر يؤطّر بدوره). النمط *1..2 يُمهِّد للوحدة 12: حين نُحمِّل رسم News، ستأخذ المسارات كامل معناها.

قوّة *min..max

(a)-[:REL*1..3]->(b) يطلب من Neo4j أن يتبع 1 أو 2 أو 3 علاقات REL موجَّهة بالترتيب. هذا ما يعوز SQL حادّةً. في الممارسة، الاحتفاظ بـحدّ أعلى يُجنِّبك استعلامات تنفجر: *1.. بلا حدّ يُكتَب تقنيًّا لكنّه مكلف.

ORDER BY وLIMIT وWHERE وأخواتها

يستعير Cypher الكلمات المفتاحيّة المألوفة من SQL بالدلالة نفسها تقريبًا. لهذا الجسر أثر مباشر: من يعرف SQL يقرأ الجزء العلويّ من استعلام Cypher دون شعور بالغربة، ثمّ يعثر تدريجيًّا على النكهة الخاصّة بالرسوم البيانيّة في المسارات والأنماط.

MATCH (p:Personne)
WHERE p.anciennete > 2 AND p.role CONTAINS 'data'
RETURN p.nom, p.role
ORDER BY p.anciennete DESC
LIMIT 3;

الإنشاء بـCREATE، والتعديل بـSET، والحذف بـDELETE أو DETACH DELETE. هذه الأخيرة حاسمة: يفشل DELETE إن كانت للعقدة علاقات بعد، بينما DETACH DELETE يحذف العقدة مع كلّ علاقاتها دفعةً واحدة.

// إضافة كفاءة، وتصحيح دور، ورفع تأطير
CREATE (:Competence {nom: 'Docker'});
MATCH (p:Personne {nom: 'Sami'}) SET p.role = 'data engineer senior';
MATCH (:Personne {nom: 'Karim'})-[r:ENCADRE]->(:Personne {nom: 'Sami'}) DELETE r;

لا تفعل قطّ MATCH (n) DELETE n على قاعدة حقيقيّة: لا بدّ إمّا من DETACH DELETE، وإمّا من مسحٍ على دفعات — وهذا بالضبط ما يفعله 99-reset.cypher بواسطة apoc.periodic.iterate (الوحدة 11).

القشرة التفاعليّة

Neo4j Browser مريح للاستكشاف والتصوير البصريّ. لأتمتة الاستعلامات أو دمجها في خطّ إنتاج، تعرض الحقيبة قشرةً أصليّةً:

./lab.sh cypher-shell

يمكن لصق كتلة Cypher فيها، إنهاؤها بـ;، ورؤية الجواب في جدول. تُعطيك الأوامر :begin و:commit و:rollback يدَ التحكّم بالمعاملات؛ و:exit يخرج من القشرة.

جرّب 1 — الكفاءات المشتركة

اكتب الاستعلام الذي يسرد الكفاءات التي يتقنها شخصان على الأقلّ من فريق Veille، مع عدد الأشخاص.

الحلّ
MATCH (:Personne)-[:MAITRISE]->(c:Competence)
WITH c, count(*) AS n
WHERE n >= 2
RETURN c.nom, n ORDER BY n DESC, c.nom;

على الرسم البيانيّ المصغَّر: Neo4j يتقنها كلٌّ من إيناس وكريم (2)، وPython يتقنها سامي وكريم (2)، وElasticsearch يتقنها إيناس وسامي (2). لا تظهر Kibana.

جرّب 2 — Elasticsearch وNeo4j معًا في Montréal

جِد مَن يُتقن في Montréal Elasticsearch وNeo4j معًا (بأيّ مستوى).

الحلّ
MATCH (p:Personne)-[:HABITE]->(:Ville {nom: 'Montréal'})
MATCH (p)-[:MAITRISE]->(:Competence {nom: 'Elasticsearch'})
MATCH (p)-[:MAITRISE]->(:Competence {nom: 'Neo4j'})
RETURN p.nom;

المنتظر: Inès (سامي لا يملك Neo4j في الرسم البيانيّ المصغَّر).

جرّب 3 — إضافة كفاءة Docker

أضِف الكفاءة « Docker » وأسندها إلى سامي بمستوى 2، ثمّ استفسر مجدّدًا عمّن يُتقن Docker بمستوى 2 أو أكثر.

الحلّ
MERGE (c:Competence {nom: 'Docker'});
MATCH (p:Personne {nom: 'Sami'}), (c:Competence {nom: 'Docker'})
MERGE (p)-[:MAITRISE {niveau: 2}]->(c);

MATCH (p:Personne)-[r:MAITRISE]->(:Competence {nom: 'Docker'})
WHERE r.niveau >= 2
RETURN p.nom, r.niveau;

المنتظر: Sami بمستوى 2.

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

  • الرسم البيانيّ أربع لبنات: العقدة، والتصنيف، والعلاقة الموسومة الموجَّهة، والخاصّيّة.
  • يستخدم Cypher أنماط ASCII للقراءة والكتابة: (a)-[:REL]->(b).
  • وضع قيد فرادة قبل البيانات يمنحك فهرسًا مجّانًا ويجعل السكربت عديم التأثير التكراريّ.
  • يقوم MERGE بـupsert، ويُدرج CREATE بلا تحقّق؛ لسكربت قابل لإعادة التشغيل: MERGE.
  • DETACH DELETE يحذف عقدة وعلاقاتها دفعةً واحدة؛ أمّا DELETE وحده فيرفض إذا كانت للعقدة علاقات.
  • يحتوي الرسم البيانيّ المصغَّر لـVeille 11 عقدة و14 علاقة: 4 أشخاص، و3 مدن، و4 كفاءات، و4 HABITE، و7 MAITRISE، و3 ENCADRE.
  • *1..2 على العلاقة يفتح المسارات ذات الطول المتغيّر — الكلمة المفتاحيّة الوحيدة التي تُغيّر كلّ شيء مقارنةً بـSQL.

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

  • يرفض Neo4j Browser كلمة المرور → المجلَّد neo4j-data أُنشئ بكلمة مرور قديمة، وتغيير NEO4J_PASSWORD في .env لا يكفي → ./lab.sh reset ثمّ ./lab.sh up.
  • ./lab.sh cypher 10-premiers-pas.cypher يُرجع «neo4j/cypher/... غير موجود» → أنت خارج مجلَّد الحقيبة → cd إلى kits/42-elasticsearch-neo4j/ قبل إعادة التشغيل.
  • :schema يعرض «No constraints» → لم يُنفَّذ السكربت أو أوقف خطأٌ الكتلةَ الأولى → ./lab.sh logs neo4j، ثمّ إعادة التشغيل بعد الإصلاح.
  • يعرض الرسم البيانيّ خمسة أشخاص بدل أربعة → استُخدم CREATE بدل MERGE في الورشة → ./lab.sh cypher 99-reset.cypher ثمّ إعادة تشغيل 10-premiers-pas.cypher.

للاستزادة

الوحدة التالية: نمذجة رسم بيانيّ وتحميله بـLOAD CSV، والقيود، والفهارس، ونمط CALL { ... } IN TRANSACTIONS، للانتقال من الرسم البيانيّ المصغَّر للفريق إلى رسم News الحقيقيّ.