الوحدة 11 — نمذجة رسم بيانيّ وتحميله: قيود، وفهارس، وLOAD CSV
الرسم البيانيّ المصغَّر لفريق Veille يقتصر على إحدى عشرة عقدة: مناسب لفهم Cypher، غير كافٍ للإنتاج. تطلب إيناس من سامي أن يُحمِّل المجموعة الحقيقيّة، وهي 200 853 مقالًا من مجموعة News، إلى Neo4j حتّى يستطيع كريم بعد ذلك كتابة نظام التوصية. تُعلّمه هذه الوحدة كيف ينتقل من جدول مسطَّح (news.csv) إلى رسم بيانيّ ذي دلالة، وكيف يضع القيود والفهارس بالترتيب الصحيح، وكيف يُحمِّل كلّ ذلك في خمس وعشرين ثانية بواسطة LOAD CSV WITH HEADERS وCALL { } IN TRANSACTIONS.
من الجدول إلى الرسم: ثلاثة أسئلة
يسرد ملفّ CSV مقالات بأعمدتها؛ ويربط الرسم البيانيّ كيانات بعضها ببعض. يتمّ العبور بثلاثة أسئلة نطرحها على كلّ عمود.
- هل سنُصفِّي أم نعبر بهذا العمود؟ إن كانت الإجابة نعم، فهو يستحقّ أن يصير عقدة. الفئة التي سنُدرجها ونُصفّيها ونتبعها عقدة؛ أمّا معرّف لا ننظر إليه أبدًا فيبقى خاصّيّة.
- هل يربط كيانَين؟ إذن هو علاقة. «المقال X نُشر في الفئة Y» ليست خاصّيّة، بل علاقة
PUBLIE_DANS. - هل نريد فقط عرضه؟ يبقى خاصّيّةً للعقدة التي ينتمي إليها. عنوان المقال وتاريخه ورابطه خصائص: لا نبحث عن «كلّ المقالات ذات هذا العنوان بالضبط»، بل نعرضها بعد إيجاد المقال.
قاعدتان في التسمية تجعلان الرسم البيانيّ مقروءًا.
- التصنيفات (Labels): كلمة واحدة بالمفرد بصيغة PascalCase —
Article،Categorie،Auteur. لا تكتبArticles، ولاarticle. - العلاقات: بأحرف كبيرة، غالبًا فعل مسنَد إلى الغائب —
PUBLIE_DANS،ECRIT_PAR،HABITE. اتّجاه السهم من الفاعل إلى المفعول: «المقال منشور في الفئة»، إذن(:Article)-[:PUBLIE_DANS]->(:Categorie).
هذه القواعد ليست زخرفيّة: فهي تجعل نمط Cypher يُقرأ كجملة. MATCH (a:Article)-[:ECRIT_PAR]->(au:Auteur) تُقرأ بلا عناء: «المقال a كتبه المؤلّف au». هذا أوّل مكسب على SQL.
نموذج Veille
ثلاثة كيانات، وعلاقتان، بلا لبس.
(:Article {id, titre, date, lien})-[:PUBLIE_DANS]->(:Categorie {nom})
(:Article)-[:ECRIT_PAR]->(:Auteur {nom})
لكلّ مقال فئة (علاقة إلزاميّة) وصفر أو مؤلّف أو أكثر (علاقة اختياريّة ومتعدّدة). خمس خصائص إجمالًا: id وnom مفتاحا فرادة، وtitre وdate وlien للعرض والترتيب. لا شيء غير ذلك. لن نُكرِّر العنوان على المؤلّف، ولا الفئة على المقال: يفعل الرسم البيانيّ ذلك عنّا. الفكرة الحاكمة: تبقى الخصائص قليلة ومحدَّدة، بينما تحمل العلاقات كامل غنى النموذج.
Categorie عن حقل category في المقال؟في Elasticsearch، يبقى category سلسلة keyword على المستند: هو الشيء الذي نبحث عنه. في Neo4j، نودّ سرد الفئات، وعدّ مقالاتها، والقفز من مؤلّف إلى فئاته المفضّلة — فنجعلها عقدة. لا يُنمذج المحرّكان الشيء نفسه لأنّهما لا يجيبان عن العائلة نفسها من الأسئلة.
ملفّ news.csv، شرط مسبق للتحميل
لا يُنزِّل سكربت Cypher شيئًا: يقرأ ملفّ CSV موجودًا مسبقًا على قرص حاوية Neo4j. يكتب هذا الملفّ مُستوردُ Elasticsearch (الوحدة 3) عند فهرسة 200 853 مستندًا.
- المجلَّد على المضيف:
kits/42-elasticsearch-neo4j/neo4j/import/news.csv. - المجلَّد داخل الحاوية:
/import/news.csv، والمسارfile:///news.csvفيLOAD CSV. - كاتبه:
./lab.sh import-news، بالتوازي مع إرسال المستندات إلى فهرسnews. - أعمدته (بهذا الترتيب، مع رأس):
id،headline،category،authors،date،link.
import-news أوّلًابدون ملفّ news.csv، يفشل LOAD CSV برسالة « Couldn't load the external resource ». التتابع الصحيح دومًا: ./lab.sh up، ثمّ ./lab.sh import-news (الذي يكتب CSV)، ثمّ فقط ./lab.sh cypher 11-charger-news.cypher. إن غاب CSV، يُشير الخطأ إلى مجلَّد /import المضاف في الحاوية: ليس Cypher هو المكسور، بل ترتيب الخطوات.
السكربت 11-charger-news.cypher، كتلةً كتلةً
تُسلّم الحقيبة neo4j/cypher/11-charger-news.cypher. ننفّذه بأمر واحد.
./lab.sh cypher 11-charger-news.cypher
على جهاز مُبعَّد صحيحًا، ينتهي السكربت في خمس وعشرين ثانية تقريبًا. لنمشِّطه سطرًا سطرًا.
الكتلة 1 — قيود وفهارس قبل البيانات
CREATE CONSTRAINT article_id IF NOT EXISTS FOR (a:Article) REQUIRE a.id IS UNIQUE;
CREATE CONSTRAINT categorie_nom IF NOT EXISTS FOR (c:Categorie) REQUIRE c.nom IS UNIQUE;
CREATE CONSTRAINT auteur_nom IF NOT EXISTS FOR (au:Auteur) REQUIRE au.nom IS UNIQUE;
CREATE INDEX article_date IF NOT EXISTS FOR (a:Article) ON (a.date);
ثلاث قيود فرادة، واحدة لكلّ تصنيف، على الخاصّيّة التي تُميِّز الكيان (id للمقال، وnom للفئة والمؤلّف). قيد الفرادة يُنشئ فهرسًا ضمنيًّا على الخاصّيّة نفسها: يصبح MERGE (a:Article {id: 42}) بحثًا بمفتاح، لا مسحًا كاملًا. بدون هذه القيود، يتحقّق كلّ MERGE على 200 853 سطرًا من الوجود بمسح، ويستغرق التحميل ساعات بدل خمس وعشرين ثانية.
الفهرس المستقلّ على Article.date ليس مطلوبًا للتحميل: يُهيِّئ للوحدة 12 حيث سنُصفّي كثيرًا حسب فترة. IF NOT EXISTS تجعل المجموعة عديمة الأثر التكراريّ — إعادة تشغيل السكربت لا تُحدث شيئًا إن كان كلّ شيء في مكانه.
القاعدة مطلقة في التحميل الضخم: أوّلًا القيد، ثمّ MERGE. وضع القيد بعد التحميل يعمل، لكنّه يُلزم Neo4j بالتحقّق لاحقًا من ملايين الأسطر. القاعدة العكسيّة — «أوّلًا البيانات ثمّ الفهارس» — قادمة من عالم SQL ولا تنطبق على Neo4j.