بعد ملاحظة سرعة وفعالية sqlite3 في ثواب 2 تبين لنا أنها الحل الأفضل
وهي قاعدة بيانات RDBM مضمنة لا تحتاج إلى خادم بل تعتمد على الملفات انظر مقالة ويكيبيديا عنها
أطلقت Sqlite عام 1421 هـ (2001 م) بعد قيام مؤلفها بأبحاث لصالح البحرية للتحكم بالمدمرات الصاروخية.
وهي تدعم معايير SQL-92 بشكل كبير.
أغلب عيوبها لا تعني مشروع ثواب بل إنها تعتبر مزايا لها. لكن عيوبها التي تعني مشروعنا هي تغيير في هيئة الملف في الإصدارات الجديدة انظر
بعد التحدث مع مطوري sqlite فإن ذلك لم يحدث بين 3.5 و 3.6 حيث أن التغيير كان في بعض أجزاء الدوال وليس في هيئة الملفات
لكن حتى لو تغيّرت هيئة الملف يمكننا أن نضع ملفات .jar التي تتعامل مع الإصدار الذي نريد.
بعد القيام بالعديد من الاختبارات الداخلية على عدد من الأمثلة إليكم نموذج البيانات الذي توصلنا له.
يجوز أن تتوزع الملفات في عدة مجلدات وتحديدا يعنينا أن يكون هناك على الأقل مكانين واحد عام للنظام لا يملك المستخدم الذي يشغل البرنامج صلاحيات التعديل فيه وآخر يفترض أن يكون له صلاحيات الكتابة حتى يحتفظ بالفهارس وأي معلومات خبيئة cached.
يحتوي كل كتاب على معلومات عن الكتاب وليست منه تسمى ميتا. ومخطط جدول الميتا في كل كتاب هو:
CREATE TABLE "meta" ( "cache_hash" TEXT, "repo" TEXT, "lang" TEXT, "kitab" TEXT, "version" TEXT, "releaseMajor" INTEGER, "releaseMinor" INTEGER, "type" INTEGER, "author" TEXT, "year" INTEGER, "originalAuthor" TEXT, "originalYear" INTEGER, "originalKitab" TEXT, "originalVersion" TEXT, "classification" TEXT );
ولا يوجد فهارس لهذا الجدول في ملف الكتاب لأنه يتكون من صف واحد. يتم تجميع هذا الصف من كل الكتب ضمن إدارة البرنامج في جدول الميتا داخل النسخة الخبيئة مع إضافة الحقول التالية uri و mtime و flags والنسخة الخبيئة تحتوي على فهارس لجدول الميتا وذلك عبر
CREATE INDEX MetaURIIndex ON meta (uri); CREATE INDEX MetaRepoIndex ON meta (repo); CREATE INDEX MetaLangIndex ON meta (lang); CREATE INDEX MetaKitabIndex ON meta (kitab); CREATE INDEX MetaKitabVersionIndex ON meta (repo,kitab,version); CREATE INDEX MetaAuthorIndex ON meta (author); CREATE INDEX MetaYearIndex ON meta (YEAR); CREATE INDEX MetaOriginalAuthorIndex ON meta (originalAuthor); CREATE INDEX MetaOriginalYearIndex ON meta (originalYear); CREATE INDEX MetaClassificationIndex ON meta (classification);
أما معاني هذه الحقول فهي
الحقل | النوع | المعنى |
---|---|---|
cache_hash | TEXT | مقطع لبصمة مميزة إن تغيرت وجب تحديث الخبيئة والفهارس |
repo | TEXT | من أي مستودع هذا الكتاب |
lang | TEXT | اللغة الغالبة في الكتاب |
kitab | TEXT | معرف نصي لاسم الكتاب |
version | TEXT | الإصدارة وهي كلما زاد يعني نسخة أحدث ومنقحة ضمن نفس المستودع وهو يزاد عند التعديلات التي تكسر الروابط |
releaseMajor | INTEGER | رقم الإطلاق الكبير وهو يزاد كلما حدث تعديلات في المحتوى لكنها لا تكسر الروابط |
releaseMinor | INTEGER | رقم الإطلاق الصغير وهو يزاد كلما حدث تعديل خارج المحتوي بل في الوسوم أو في الميتا |
type | INTEGER | النوع: 0 كتاب عادي. 1 تفسير. 2. معجم أو تراجم |
author | TEXT | معرف نصي لأول مؤلف للنص المفرغ هنا والتفاصيل تكون من جدول المؤلفين الخاص بالمستودع |
year | INTEGER | العام الهجري لوفاته، 0 إن كان على قيد الحياة |
originalAuthor | TEXT | معرف مؤلف الكتاب الأول بصورته الاولى في حال الكتب المترجمة |
originalYear | INTEGER | العام الهجري لوفاة المؤلف الأول |
originalKitab | TEXT | معرف الكتاب الذي هذا ترجمته - يترك خاليا إن لم يكن مترجما |
originalVersion | TEXT | إصدار الكتاب الأصلي وحيث أن الروابط لا تختلف عند نفس الإصدار لا يهم من أي إطلاق |
classification | TEXT | قائمة بالتصنيفات تفصل بين الفروع علامة / وتفصل بين التصنيفات علامة : |
حقول في النسخة الخبيئة فقط | ||
uri | TEXT | العنوان الفريد غالبا هو مجرد مسار لملف الكتاب |
mtime | FLOAT | وقت آخر تعديل على ملف الكتاب كما يعرضه نظام التشغيل |
flags | INTEGER | خيارات - حاليا 2 إن تمت الفهرسة بشكل صحيح 0 إن لم يكن هناك فهارس و 1 إن كان هناك فهرس غير صالح |
تم تصميم هذه البيانات كي تترتب حسب تاريخ وفاة المؤلف وإن كان حيا فإن كان تاريخ الوفاة غير معروف تترتب تلك الكتب في آخر القائمة هجائيا. ويعود للمستخدم الاختيار بين المؤلف الأصلي أم المترجم إن كان الكتاب مترجما فإن لم يكن الكتاب مترجما يفترض أن تكون القيمتين ذاتها.
يتكون أي كتاب من 3 جداول أساسية إضافة إلى جدول الميتا. هذه الجداول هي:
وبلغة SQL المخطط هو
CREATE TABLE "nodes" ( "idNum" INTEGER PRIMARY KEY NOT NULL, "content" TEXT, "parent" INTEGER, "globalOrder" INTEGER, "depth" INTEGER NOT NULL ); CREATE TABLE "tags" ( "idNum" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" VARCHAR NOT NULL, "flags" INTEGER NOT NULL, "comment" VARCHAR, "parent" INTEGER, "relation" INTEGER ); CREATE TABLE "nodesTags" ( "tagIdNum" INTEGER NOT NULL, "nodeIdNum" INTEGER NOT NULL, "param" VARCHAR, PRIMARY KEY ("tagIdNum", "nodeIdNum") ); CREATE INDEX NodesParentIndex ON nodes (parent); CREATE INDEX NodesNodesGlobalOrderIndex ON nodes (globalOrder); CREATE INDEX NodesDepthIndex ON nodes (depth); CREATE INDEX NodesTagTagIdNumIndex ON nodesTags(tagIdNum); CREATE INDEX NodesTagNodeIdNumIndex ON nodesTags(nodeIdNum); CREATE INDEX NodesTagParamIndex ON nodesTags(param); CREATE INDEX TagsName ON tags (name);
تلك الحقول تعني ما يلي:
جدول العقد nodes | ||
---|---|---|
idNum | INTEGER | معرف عددي فريد تلقائي |
content | TEXT | محتوى العقدة |
parent | INTEGER | معرف والد العقدة في شجرة الهرمية |
globalOrder | INTEGER | ترتيب الصف عند سبر الشجرة بطريقة العمق أولا |
depth | INTEGER | عمق العقدة - الجذر صفر وأول أولاده 1 |
جدول الوسوم tags | ||
idNum | INTEGER | معرف عددي فريد تلقائي |
name | TEXT | اسم الوسم |
flags | INTEGER | علامات تحدد نوع الوسم وسلوكه |
comment | TEXT | تعليق يشرح للمحررين كيفية استعمال هذا الوسم |
parent | INTEGER | والد هذا الوسم |
relation | INTEGER | علاقة الوسم |
العلامات هي
TAG_FLAGS_EXTERNAL_SOURCE | محتويات خارجية أي تجلب من خارج الكتاب |
---|---|
TAG_FLAGS_BYBOT | هذه العقدة وأحفادها مولدة آليا أي تعديلات فيها ستفقد عندما يعاد توليدها |
TAG_FLAGS_HEADER | هذه العقدة تمثل تبويب أي يجب أن يفهرس هو وأحفاده في حقل وثيقة فهرسة مستقلة ثم يتم استهلاك المحتويات بعد الفهرسة |
TAG_FLAGS_IX_TAG | هذه العقدة تحمل وسم بحث |
TAG_FLAGS_IX_FIELD | حقل فهرسة منفصل: أي أن هذه العقدة وأحفادها تفهرس مرة أخرى لكن في وثيقة فهرسة مستقلة أي لا يتم استهلاك المحتويات بعد الفهرسة |
TAG_FLAGS_IX_SKIP | تجاهل فهرسة محتويات العقدة وأحفادها |
TAG_FLAGS_PAD_CONTENT | تحتاج هذه العقدة أن تعزل عن سياقها بمسافة خالية whitespace إما مسافة أو سطر جديد في حالة TAG_FLAGS_FLOW_MASK |
TAG_FLAGS_FLOW_BLOCK | العقدة تكون في صندوق منفصل عن سير فقرة السياق |
TAG_FLAGS_FLOW_FLOAT | عقدة طافية |
TAG_FLAGS_FLOW_FOOTER | عقدة تذيل كالحواشي |
TAG_FLAGS_FLOW_HIDDEN | عقدة خفية. تعليق مثلا |
ملاحظات
تم اختباره عبر عمل سكربت يحول نص بطريقة تشبه الويكي إلى قاعدة بيانات بالنموذج المختار. وكانت النتيجة كما يلي:
الملف | الحجم كملف نصي | حجم النموذج | في الشاملة | في ثواب 2 |
---|---|---|---|---|
موطأ الإمام مالك | 2213 ك.ب | 2679 ك.ب | 12870 ك.ب | 2732 ك.ب |
صحيح البخاري | 9296 ك.ب | 11275 ك.ب | - | 11291 ك.ب |
طبعا تم تحديد الأبواب والنص دون وسم السند والمتن وشجرة الرواة …إلخ.
استخراج جدول المحتويات كاملا لصحيح البخاري (11 ميغا) يستغرق أقل من جزء من عشرة من الثانية. علما أن الصفوف ليست مخزنة بشكل متسلسل!
time sqlite3 bukhari.db \ 'SELECT * FROM nodes LEFT OUTER JOIN nodesTags ON nodes.id = nodesTags.nodeId LEFT OUTER JOIN tags on nodesTags.tagId=tags.id WHERE tags.name="header"; ' >/dev/null
على فرض أن كتاب الإيمان يحمل المعرف node.id=3 وبه حوالي 20 باب لكل باب صفين في قاعدة البيانات عقدة تحمل وسم التبويب header وأخرى متفرعة عنها تحمل المحتويات. لاستخراج باب الإيمان كله وأحفاده
time sqlite3 bukhari.db 'SELECT nodes.id, nodes.content FROM nodes LEFT OUTER JOIN nodesTags ON nodes.id = nodesTags.nodeId LEFT OUTER JOIN tags on nodesTags.tagId=tags.id WHERE nodes.l2=3; ' >/dev/null real 0m0.016s user 0m0.001s sys 0m0.003s
أي أنها أقل من جزء من مئة جزء من الثانية.
ولنفرض أن الصف رقم 31 يحتوي على باب الحياء شعبة من الإيمان لاستخراج محتوياته
time sqlite3 bukhari.db 'SELECT nodes.id, nodes.content FROM nodes LEFT OUTER JOIN nodesTags ON nodes.id = nodesTags.nodeId LEFT OUTER JOIN tags on nodesTags.tagId=tags.id WHERE nodes.l3=31; '
وقد كان الوقت أقل من جزء من ألف من الثانية.
لاحظ أننا نستخدم l2 أو l3 … بحسب عمق العقدة.
وقمت بتجربة الوقت اللازم لاستيراد كتاب بسيط من هيئة wiki النصية إلى نموذج البيانات 10 مرات ثم تم تكرار التجربة على صحيح البخاري 10 مرات هو الآخر.
بأمر يشبه ما يلي:
time for i in `seq 1 10`; do groovy source/Wiki2Db.groovy samples/test.txt; done
فكانت النتيجة
الطريقة | زمن الكتاب البسيط | زمن صحيح البخاري |
---|---|---|
بايثون | 0.477 | 27.857 |
جروفي | 11.259 | 151.007 |
جافا .class | 8.869 | 187.752 |
جروفي التكرار داخلها* | 1.900 | 178.089 |
جافا . class التكرار داخلها* | 1.632 | 180.584 |
لكن يبدو أن سبب تأخر جافا هو الوقت اللازم لتحميل جافا فلو وضعنا الحلقة التكرارية للاستيراد 10 مرات داخل الكود يكون الزمن أقل بكثير ومع أن هذا ظلم لبايثون حيث أننا قمنا بتحميل مفسر بايثون 10 مرات إلا انها ظلت متفوقة ويمكن ملاحظة التفوق عند النظر إلى الزمن عند استيراد ملف كبير مثل صحيح البخاري حيث أصبح زمن تحميل جروفي هامشيا.
وسبب تفوق بايثون إما استعمالها لمكتبة سي الخاصة ب sqlite مباشرة أو بسبب اغلاقي للخيار isolation_level في بايثون دون جروفي
cn=sqlite3.connect(fn, isolation_level=None)
بما أن xml لا تحقق أهداف التصميم لمشروعنا حيث لا يمكن التعامل معها بطريقة تزايدية أو جزئية ومن الصعب القفز فيها. لذا علينا درساة طرق بديلة ل xml ما يهما بشكل أساسي هو عملية Serialization لعدة أسباب منها
ملاحظات على كل منها