العودة إلى التحديثات
New releaseAug 5, 2026

slater v0.24.4

قاعدة بيانات رسومية منخفضة استهلاك الذاكرة مع دعم Bolt+TLS، وتشفير عند التخزين، ومتجهات مصممة لحالات استخدام النسخ المتماثل المحلي للرسوم البيانية.

مشاركة

Slater

CI Release

الإصدار الحالي: v0.24.4جميع الإصدارات.

في سطر واحد: يخدم سلاتر رسومًا بيانية لا تتسع في الذاكرة — مئات الملايين من العقد ومليارات الحواف في بضع مئات قليلة من MB من RAM — عبر Bolt القياسي، بحيث يعمل أي برنامج تشغيل neo4j دون تعديل، مع بحث متجهي أصلي على القرص بجوار الرسم البياني، ويقبل كتابات حية ودائمة دون التخلي عن ذلك. الذاكرة المقيمة تحدّدها ميزانية كاش تختارها أنت، وليس حجم الرسم البياني.


اختصارات

لماذا يوجد سلاتر

قاعدة البيانات الرسومية تخزّن البيانات كـ أشياء (عُقد) والعلاقات بينها (حواف)، مع اعتبار العلاقات مواطنًا من الدرجة الأولى. هذا ما تريده عندما تكون أسئلتك عن الاتصالات بدلاً من الصفوف — «من على بُعد ثلاث قفزات من هذا الحساب؟»، «ما سلسلة الاعتماد الكاملة خلف هذا البناء؟»، «أي الحسابات تشترك في جهاز وعنوان وبطاقة؟» — الاستعلامات التي تتحول في SQL إلى مستنقع من الانضمامات العودية، لكنها تخرج بشكل طبيعي في الرسم البياني.

الشكوى الأكثر شيوعًا حول قواعد البيانات الرسومية هي أنها لا تتوسع إلى ما يتجاوز ما يمكنك الاحتفاظ به في RAM. كثير منها (مثل neo4j وMemgraph وFalkorDB وغيرها) يُبقي الرسم البياني كاملًا في الذاكرة: رسم بياني بحجم 40 GB يتطلب 40 GB من الذاكرة — لكل مثيل. هل تريد نسخة متماثلة لكل منطقة أو لكل مستأجر أو لكل pod؟ اضرب الفاتورة. وبعد حجم معيّن، لن تُحمَّل أصلًا: مثل رسم Wikidata البياني الذي يضم 90 مليون عقدة / 1.5 مليار حافة، والذي يتطلب ~64–128 GiB مقيمة، لذلك لا تستطيع محركات الذاكرة فتحه إطلاقًا.

سلاتر هو الرد. بدلاً من تحميل الرسم البياني إلى الذاكرة، يُجمّعه مرة واحدة، دون اتصال: يحوّل slater-build بياناتك إلى صورة ثابتة على القرص بعنونة محتوى، وأي عدد من خوادم سلاتر يخدم بعد ذلك تلك الصورة عبر Bolt (إذ تعمل برامج تشغيل neo4j الحالية لديك دون تغيير)، مع ترحيل الكتل عند الطلب والاحتفاظ بميزانية كاش ثابتة فقط في الذاكرة. هكذا يخدم الرسم البياني نفسه ذو 90 مليون عقدة من بضع مئات من MB من RAM — حجم الرسم البياني وفاتورة الذاكرة مفصولان. رسم بياني بحجم 4 GB ورسم بياني بحجم 400 GB يكلفان نفس RAM للخدمة، لذا يمكنك نشر نسخ قراءة متماثلة رخيصة وعديمة الحالة وترك التخزين، وليس الكومة، هو ما يحمل الرسم البياني.

هذا يجعله مناسبًا طبيعيًا للرسوم البيانية المعرفية خلف RAG، ورسوم التوصيات والهوية، ورسوم الاعتماديات — أي شيء كبير ومترابط تريد الاستعلام عنه بتكلفة منخفضة وبشكل متكرر. البحث المتجهي الأصلي على القرص يقع بجوار الرسم البياني مباشرة، بحيث يكون المحرك نفسه هو طبقة الاسترجاع للتضمينات أيضًا.

لكن التجميع مرة واحدة لا يعني التجمد. تلك الصورة هي أساس، وليست حالة نهائية: طبقة كتابة اختيارية تقع فوقها، بحيث يمكن تصحيح الرسم البياني الحي وتوسيعه دون إعادة بناء أي شيء.

القراءات والكتابات

النواة غير قابلة للتغيير؛ الرسم البياني ليس كذلك. فعّل الطبقة القابلة للكتابة (delta.enabled) وستكتب عبر Bolt — صحّح خاصية واحدة، أضف عقدة، اسحب حافة — ويستقر التغيير بشكل دائم، دون إعادة بناء الصورة. ما يُبقي الأمر رخيصًا في جانب القراءة هو مكان وجود الكتابات.

تتراكم الكتابات في طبقة دمج منظمة بالسجلات (LSM) فوق النواة غير القابلة للتغيير: سجل كتابة مسبق وجدول داخل الذاكرة، يفيضان إلى شرائح دلتا غير قابلة للتغيير، تُدمج مرة أخرى إلى نواة جديدة عبر توحيد دوري. ماذا يوفّر لك ذلك:

  • القراءات عبر رسم بياني غير مكتوب تكلف بالضبط ما كانت تكلفه سابقًا. الدلتا الفارغة هي فرع واحد قابل للتنبؤ، وليست دمجًا — مسار القراءة متطابق بايتًا ببايت سواء كانت الطبقة القابلة للكتابة مفعّلة أم لا.
  • تكلفة قراءة الكتابة تتناسب مع حجم الدلتا، وليس مع حجم الرسم البياني. الإجابات على كامل الرسم البياني — count(*), والإحصائيات الهامشية لنوع التسمية والعلاقة — تبقى قراءات بيانات وصفية حتى مع وجود كتابات معلّقة: تحتفظ الدلتا بعداداتها الخاصة، لذا فإن count(*) على نواة من 91.6 مليون عقدة مع نصف مليون كتابة معلّقة يجيب في غضون عشرات المللي ثانية دون لمس أي كتلة.
  • الإقرار يعني الديمومة. كاتب واحد يفرّغ قائمة الانتظار ولا يعيد SUCCESS إلا بعد fsync الذي يغطي الكتابة. جمّع كتاباتك وستكون رخيصة — كتابة UNWIND تلتزم بنداء fsync واحد لكل دفعة بدلاً من كل صف.
  • كتابات بمفتاح العمل، بأي من الصيغتين. MERGE / MATCH … SET / DELETE (وكذلك CREATE / REMOVE, والحذف بعد الفصل، وكتابات العلاقات) مفتاحية بخصائص هوية العقدة — أو عبارات تعديل البيانات المكافئة في ISO GQL (INSERT / SET / REMOVE / DELETE), التي تنزل إلى المسار نفسه. صحّح، وأدرج، ونفّذ upsert, واسحب، عبر العقد والحواف، بالطريقة التي تُخاطب بها بياناتك فعلًا.

مع إيقاف الطبقة — وهو الوضع الافتراضي — يخدم سلاتر النواة غير القابلة للتغيير النقية ويرفض الكتابات. راجع الطبقة القابلة للكتابة للنموذج الكامل.

عن الاسم. سُمي سلاتر تيمنًا بعميل وكالة المخابرات المركزية في Archer (مسلسل رائع) الذي يصر على الاكتفاء باسم واحد — «Just… Slater» — وهو أحد شخصياتي المفضلة فيه. انظر صفحة الويكي الخاصة بالشخصية.

ما الذي ستحصل عليه

  • RAM تحدده ميزانية الكاش التي تختارها، وليس حجم الرسم البياني — انشر عدد ما تشاء من نسخ القراءة المتماثلة؛ لا يجب أبدًا أن يتسع الرسم البياني في الذاكرة.
  • بديل متوافق للرسم البياني — يتحدث Bolt، لذا فإن أي برنامج تشغيل neo4j قياسي (JS، Python، Go…) يعمل دون تغيير. إنها Cypher (بالإضافة إلى شريحة من ISO GQL، للقراءة والكتابة)؛ لا شيء جديد لتتعلمه.
  • كتابات حية ودائمة — طبقة LSM اختيارية فوق النواة غير القابلة للتغيير: MERGE / SET / DELETE بمفتاح العمل عبر العقد والحواف، بثبات جماعي وديمومة عبر fsync, وتُدمج مرة أخرى في نواة جديدة عبر التوحيد. القراءات لا تدفع ثمن ذلك.
  • نشر عبر استبدال الملفات — ابنِ جيلًا جديدًا بعنونة محتوى دون اتصال، واقلب مؤشر current بشكل ذري، وسوف تلتقطه الخوادم. كل كتلة مُدققة بمجموع اختباري، بحيث يتم رفض الصورة المنسوخة جزئيًا بدلاً من تقديمها.
  • بحث متجهي مدمج — تقريب أقرب جار أصلي على القرص (cosine أو L2 أو dot KNN) يقع بجوار الرسم البياني مباشرة، لتكون هذه هي طبقة الاسترجاع خلف خط أنابيب RAG، والتضمينات قابلة للكتابة في مكانها — دون إعادة بناء دون اتصال لإضافة متجه أو تغييره.
  • مقفلة بالتصميم — صلاحيات القراءة والكتابة مستقلة، بالإضافة إلى تشفير اختياري أثناء التخزين، وTLS Bolt، وقوائم ACL مبوبة بـ argon2id، ونظام ملفات جذر للقراءة فقط لنسخ القراءة المتماثلة. عيّن مفتاحًا رئيسيًا وستكون الصورة على القرص موثَّقة وكذلك مشفرة — يحمل بيانها MAC بمفتاح، بحيث لا يمكن لمهاجم لديه وصول كتابة إلى دليل البيانات وليس بحوزته المفتاح أن يزور بيانًا سيقبله الخادم. بدون مفتاح، لا تزال تحصل على تجزئة المحتوى، التي تكتشف الصورة المنسوخة جزئيًا أو التالفة — وليس المتعمدة. ما الذي توفره كل تهيئة.

الميزات

الميزةماذا تعني لك
ذاكرة محدودة ويمكن التنبؤ بهاتتبع الذاكرة المقيمة ثلاث ميزانيات كاش أنت تحدّدها، ضمن حمل زائد محدود لكل إدخال ومن المخصص — لا تنمو مع حجم الرسم البياني؛ أنت تضبط المفاضلة بين الأداء وRAM بدلاً من تجهيز الرسم البياني بأكمله. مخصص jemalloc مع تطهير في الخلفية يعيد الذاكرة المحررة إلى نظام التشغيل بعد انفجارات الاستعلامات الثقيلة، بحيث تعود الذاكرة المقيمة نحو قاعدتها الخاملة بدلاً من البقاء مثبتة عند المستوى المرتفع بعد الانفجار.
متعدد المستأجرين خارج الصندوقخادم واحد يستضيف العديد من الرسوم البيانية مع صلاحيات قراءة لكل مستخدم — عزل متعدد قواعد البيانات تحتفظ به معظم قواعد البيانات الرسومية لمستوى مدفوع/مؤسسي.
تشفير أثناء التخزين وأثناء النقلختم لكل كتلة بـ XChaCha20-Poly1305 (لا يُكتب المفتاح أبدًا على القرص) بالإضافة إلى TLS اختياري (bolt+s://). متوافق مع GDPR منذ التصميم. التشفير هو أيضًا ما يشتري سلامة موثَّقة: يختم البنّاء البيان بـ MAC بمفتاح، ويقوم الخادم الذي يحمل المفتاح بالتحقق منه ويرفض خدمة جيل تم تزوير بيانه أو تغييره أو نزع MAC منه. الصورة غير المفتاحية (نص صريح) محمية بتجزئة المحتوى غير المفتاحية فقط — للاكتمال والتلف، وليس للعبث. انظر ما الذي تعنيه السلامة في كل تهيئة.
تثبيت ضئيلملف ثنائي صغير ومجرد على أساس glibc distroless (بدون shell/apt) — صورة متعددة البنى (amd64/arm64) تُسحب بحوالي 22 MB، أو حوالي 12 MB للإصدار slater:latest-lite الذي يضم الخادم فقط؛ TLS نقي بـ Rust، بدون OpenSSL. اسحب وشغّل.
مُصمم للنشر الدوريابنِ رسمًا بيانيًا دون اتصال، واخدمه كصورة غير قابلة للتغيير، ثم استبدل إصدارًا جديدًا بشكل ذري دون انقطاع — مثالي لأحمال عمل مستودع البيانات / التحديث المجدول.
متين تحت الضغطيُترجم الخادم والبنّاء دون اتصال مع #![forbid(unsafe_code)] — الـ unsafe الوحيد في المحرك يعيش داخل crate المخصص jemalloc المُدقق. النواة غير قابلة للتغيير، لذا لا تأخذ القراءات أقفالًا ولا تنتظر كاتبًا أبدًا؛ كاتب واحد يسلسل الطفرات خلف مسار الكتابة وحده. لا توقفات لجمع القمامة، ولا سباقات بيانات. استعلام سيئ واحد لا يمكنه إسقاط الخادم.
يعمل مع أدوات neo4j الخاصة بكيتحدث Bolt 5.4 / 4.4 / 4.1 — استخدم برامج تشغيل neo4j القياسية (JS، Python، Go، Java…), أو cypher-shell, أو متصفحات الرسوم البيانية دون تغيير.
سطح استعلام Cypher غنيسطح قراءة واسع: MATCH/WHERE/WITH/UNION, واستعلامات فرعية CALL {…}, وأكثر من 70 دالة وتجميعًا، وقيمًا زمنية وجغرافية مكانية، وتعبيرات نمطية.
كتابات حية ودائمةطبقة LSM اختيارية بكاتب واحد فوق النواة غير القابلة للتغيير (delta.enabled): MERGE / SET / DELETE / CREATE / REMOVE بمفتاح العمل عبر العقد والعلاقات، وكتابة UNWIND مجمَّعة (fsync واحد لكل دفعة), وCALL slater.consolidate() — مثبتة جماعيًا، دائمة عبر fsync, وتُدمج في نواة جديدة عبر التوحيد. مسار القراءة مطابق بايتًا ببايت عندما تكون الدلتا فارغة.
ISO GQL، قراءة وكتابةيتحدث مجموعة فرعية من ISO GQL (ISO/IEC 39075) عبر نفس اتصال Bolt — المسارات الكمية، ومقيّدات المسار، ومحددات أقصر مسار، والتعبيرات المنطقية للتسمية/النوع، وFOR, وCAST, وبادئة لهجة اختيارية GQL/CYPHER — ومع تفعيل الطبقة القابلة للكتابة، تنزل جمل تعديل البيانات في GQL (INSERT / SET / REMOVE / [DETACH] DELETE) إلى نفس مسار الكتابة الدائم. Cypher وGQL، قراءة وكتابة، في محرك واحد.
متجهات + رسم بياني في محرك واحدبحث متجهي تقريبي للأقرب الجار أصلي على القرص (Vamana + PQ; cosine / L2 / dot) للتضمينات/RAG، بالإضافة إلى خوارزميات رسوم بيانية (PageRank، BFS، betweenness، WCC…) — ذاكرة محدودة حتى مع ملايين المتجهات. التضمينات قابلة للكتابة (سلم كتابة بأسلوب FreshDiskANN): أدخل / حدّث / احذف متجهًا، يظهر عبر KNN فورًا، ويُدمج في القاعدة دون إعادة بناء.
آمن على تخزين الشبكةكل ملف مُجزّأ بالمحتوى باستخدام BLAKE3 ويُتحقق منه عند الفتح؛ الصور الممزقة أو المنسوخة جزئيًا تُرفض ولا تُقدَّم. مصمم لأحجام NFS/الشبكة البعيدة (بدون مفاجآت mmap).
خلفيات تخزين قابلة للتوصيلاخدم نفس تنسيق الجيل من نظام ملفات محلي، أو حاوية S3 (متوافقة مع S3), أو حاوية Google Cloud Storage — انشر مرة واحدة ووزّع على نسخ متماثلة عديمة الحالة — مع طبقة كاش اختيارية على قرص SSD محلي أمام مخزن الكائنات. انظر الخلفيات التخزينية.

يتألف هذا المشروع من ملفين ثنائيين:

الملف الثنائيالدور
slaterخادم Bolt عبر الإنترنت (نقطة إدخال الحاوية ENTRYPOINT): يخدم القراءات ومع delta.enabled مسار الكتابة الدائم بكاتب واحد.
slater-buildالمُجمِّع دون اتصال: يحوّل تفريغ Cypher بدائي إلى دليل جيل ثابت ومجزأ بالمحتوى.

يقسّم سلاتر البناء بالجملة عن الخدمة: slater-build يقوم بالعمل الشاق دون اتصال — استيعاب بياناتك وتجميعها في جيل غير قابل للتغيير — بحيث لا يُجمّع رسم بياني بارد أبدًا على المسار الساخن للخدمة. داخل الخادم، يجيب سطح القراءة على شريحة واسعة من Cypher — مطابقة الأنماط، واستعلامات فرعية WITH/UNION/CALL {…}, وأكثر من 70 دالة عددية وتجميعية، وقيمًا زمنية وجغرافية مكانية، وخوارزميات رسوم بيانية (algo.*), وKNN المتجهي الأصلي على القرص (db.idx.vector.queryNodes) — بينما تقع طبقة دلتا القابلة للكتابة أسفل ذلك السطح وتكون بتكلفة صفرية عندما تكون فارغة، لذا لا تحمل القراءات أبدًا آلية جانب الكتابة. يمكنك تحديث رسم بياني بطريقتين: الكتابة إليه مباشرة عبر Bolt (انظر الطبقة القابلة للكتابة)، أو بناء جيل جديد دون اتصال واستبدال مؤشر current بشكل ذري، وهو ما يلتقطه الخادم الجاري عبر حارس الجيل (انظر حارس الجيل).

التوثيق

الدليل الكامل للمستخدم موجود في docs/manual/ — دليل ميزة بميزة يشرح، لكل إمكانية، ما هي عليه، ولماذا توجد، وكيفية استخدامها، مع أمثلة عملية يمكنك تشغيلها مقابل رسم بياني تجريبي مرفق. ابدأ هناك لأي شيء يتجاوز هذه النظرة العامة.

التشغيل مع Docker

صُمم سلاتر ليُشغَّل كـ نشر Docker — هذه هي الطريقة المتوقعة لاستخدامه. تُنشر الصور متعددة البنى الجاهزة (linux/amd64 + linux/arm64) على Docker Hub في hikarisystems/slater, بوسوم :latest و:vX.Y.Z في كل إصدار:```sh docker pull hikarisystems/slater:latest

دليل استخدام وتهيئة وعمليات يقتصر على أوامر Docker فقط موجود في
[`DOCKERHUB.md`](https://github.com/hikari-systems/slater/blob/HEAD/DOCKERHUB.md) (وهو معكوس على صفحة نظرة عامة في Docker Hub) —
**ابدأ هناك إذا كنت تنشر.** باختصار:```sh
# Build a graph generation with the offline writer:
docker run --rm -v slater-data:/data -v "$PWD/dumps:/dumps:ro" \
  --entrypoint /app/slater-build hikarisystems/slater:latest \
  --input /dumps/people.cypher --graph people --data-dir /data

# Serve it over Bolt on 7687 (read-only unless `delta.enabled`):
docker run -d --name slater -p 7687:7687 \
  -v slater-data:/data:ro -v "$PWD/acl.json:/config/acl.json:ro" \
  hikarisystems/slater:latest

لبناء الصورة محليًا بدلاً من ذلك (مثل: للتطوير):```sh

Build the image (both binaries).

docker compose build

Serve (expects generations under the slater-data volume / your /data mount).

docker compose up slater

Build a generation with the offline writer (profile build):

docker compose run --rm builder
--input /dumps/people.cypher --graph people --data-dir /data

تقوم مرحلة البناء بتثبيت `cmake` و`clang` و`libclang-dev` للخلفية `aws-lc-rs` الخاصة بـ rustls؛ كما أن `git` (الموجود بالفعل في الصورة الأساسية) مطلوب للاعتماد `hs-utils` من نوع git+tag، والذي يقوم `.cargo/config.toml` بجلبه عبر واجهة أوامر git.

تغطي الأقسام التالية التنسيق المخزّن على القرص، والإعدادات، وقوائم التحكم بالوصول (ACLs)، ومثالًا عمليًا محليًا (بدون Docker).

## كيف يعمل```
            slater-build                         slater (Bolt server)
   dump.cypher ──────────▶ /data/<graph>/<uuid>/ ──────────▶ neo4j driver
   (offline, atomic)        MANIFEST.json, *.blk,            (bolt / bolt+s)
                            range/*.isam, vector/*.{vamana,pq},
                            current → <uuid>
  • الجيل (generation) هو دليل واحد غير قابل للتغيير: ملف MANIFEST.json (جداول رموز، واصفات فهارس، ترويسة تشفير اختيارية)، ملفات كتلة عمودية (node_props.blk, node_labels.blk, edge_props.blk, topology.csr.blk, vectors.f32.blk)، فهارس نطاق (range/<name>.isam)، فهارس تقريب الجار الأقرب فوق العتبة (ANN) (vector/<label>.<prop>.{vamana,pq})، ومؤشر نصي current.
  • كل كتلة مضغوطة بـ zstd ومُتحقق منها عبر BLAKE3؛ ومع --encrypt تُغلَّف كل كتلة إضافيًا بـ XChaCha20-Poly1305 (AEAD عند الثبات).
  • يفتح الخادم جيلًا عن طريق إعادة تجزئة كل ملف ومقارنتها بالمانيفست، لذا فإن صورة منسوخة جزئيًا / مقطوعة — نسخة ممزقة إلى دليل البيانات، الذي قد يكون تخزينًا بعيدًا/شبكيًا — تُرفض بدلًا من تقديمها.
  • تتدفق القراءات عبر ثلاث مجموعات تخزين مؤقت محدودة — LRU للكتل المفكوكة، ومجموعة فهارس المتجهات (رموز PQ المقيمة + LRU لكتل Vamana)، وLRU للنتائج — ولكل منها ميزانية بايتات خاصة بها. تزن كل مجموعة ما تحمله وتطرُد العناصر لتبقى ضمن ميزانيتها، لذا يتتبع حجم المجموعة المقيمة (RSS) الميزانيات ضمن حمل ثابت لكل إدخال ونفقات عامة للمُخصِّص بدلًا من أن ينمو مع الرسم البياني.

الطبقة القابلة للكتابة

مع تفعيل delta.enabled، يصبح الجيل غير القابل للتغيير المستوى السفلي المضغوط بالكامل (النواة "core") لشجرة دمج صغيرة منظمة بالسجلات (log-structured merge tree)، وتعلو فوقه الكتابات المباشرة:``` write (Bolt) read (Bolt) │ │ ▼ ▼ ┌──────────────┐ flush ┌──────────────┐ ┌──────────────────────┐ │ WAL + active │ ───────▶ │ L0 delta │ │ a query pins one │ │ memtable │ │ segments │ │ (core, delta) view │ └──────────────┘ └──────┬───────┘ │ and reads the merge │ (fsync = ack) │ └──────────────────────┘ consolidation │ (folds core + delta → fresh core) ▼ ┌─────────────┐ │ new core │ (atomic current swap) └─────────────┘

* **أرضية المتانة — WAL.** كل طفرة تُسلسَل خلف كاتب واحد
  لكل رسم بياني، وتُلحق بسجل كتابة مسبق خاص بكل رسم بياني، ويُنفَّذ `fsync` قبل
  إرجاع `SUCCESS` من Bolt — أي أن *التأكيد يعني المتانة*، ويُتجاهَل ذيل ممزق
  عند إعادة التشغيل. كتابة دفعية عبر `UNWIND` تُلحق صفوفها وتُثبّت **واحدة**
  `fsync` للدفعة كلها. إن WAL **على القرص المحلي فقط** (لا يُمرَّر عبر
  الواجهة الخلفية للتخزين)، وهذا يجعل عقدة *الكاتب* ذات حالة: تحتاج وحدة تخزين
  محلية دائمة في `delta.walDir`. نسخ القراءة تبقى بلا حالة.
* **Memtable → L0 → الدمج.** تتراكم الكتابات في جدول ذاكرة (memtable) داخل
  RAM (محدود بـ `delta.memtableBytes`)؛ وعند امتلائه يُفرَّغ إلى مقطع دلتا L0
  غير قابل للتغيير. **الدمج (consolidation)** يطوي `{core + delta}` في نواة
  جديدة عبر تسلسل العرض المدمج مرة أخرى من خلال `slater-build` واستبدال `current`
  ذريًا — نفس حارس تجزئة المحتوى كما في أي صيغة (generation) منشورة. يمكن
  تشغيله يدويًا عبر `CALL slater.consolidate()`، أو تلقائيًا عند بلوغ
  `delta.deltaCorePercent` من حجم النواة (اختياريًا مقيدًا بنافذة خارج
  أوقات الذروة `delta.consolidateWindow`)، أو اترك حد `delta.deltaHardBytes`
  ليكبح النمو الجامح.
* **الطبقة الفوقية (overlay) تقع تحت سطح القراءة.** يقرأ المُنفِّذ عبر
  `ReadView` الذي يكون إما النواة المجردة (دلتا فارغة دائمًا) أو عرضًا مدمجًا
  `(core, delta)`؛ وتُفرد المحرّك (engine) عليه نمطيًا، لذا فإن دلتا فارغة
  تُصرَّف إلى فرع واحد يمكن التنبؤ به، ويكون مسار القراءة فقط مطابقًا
  بايتًا ببايت. عدادات الرسم البياني بالكامل (`count(*)`, هوامش
  التسميات/العلاقات) تُقدَّم من العدادات الحية الخاصة بالدلتا، فتبقى قراءات
  بيانات وصفية حتى مع وجود كتابات معلقة.
* **الاستعلام يرى لقطة ثابتة.** يثبّت زوجًا واحدًا `(core, delta)` طوال
  عمره. لا توجد معاملات متعددة العبارات ولا تراجع (rollback) — الكتابة هي
  تصحيح دائم موجَّه بمفتاح عمل (business key)، وليست معاملة OLTP.

صيغة الكتابة الدقيقة ومقابض الضبط موجودة في جدول [Configuration](#environment--configuration)
(`delta.*`) و[المثال العملي](#worked-example) أدناه.

### فهارس النطاق (ISAM)

فهرس النطاق (`range/<name>.isam`, واحد لكل `(label, property)` مفهرس) يتيح
لـ `MATCH (n:Label {prop: v})` أو `WHERE n.prop <op> v` أن يُحل إلى معرّفات
العقد المطابقة **بدون فحص التسمية**. وهو بنية
**[ISAM](https://en.wikipedia.org/wiki/ISAM)** (Indexed Sequential Access Method)
— الفهرس *الثابت والمفروز وذو البنية الكتلية* الكلاسيكي، وهو الشكل المناسب
تمامًا لصيغة غير قابلة للتغيير: لا توجد إدراجات تحتاج إعادة توازن، لذا فإن
بساطة ISAM تمنح ما كانت آليات تحوير شجرة B ستعقّده فقط.

* الإدخالات `(value, entity_id)` مفروزة حسب القيمة ومعبأة في نفس
  كتل 256 KiB المضغوطة بـ zstd مثل كل شيء آخر.
* **مستوى علوي مقيم (resident)** صغير يحمل المفتاح الأول لكل كتلة (فهرس
  متناثر). يقوم البحث الثنائي في ذلك المستوى العلوي في الذاكرة بتحديد *الكتلة
  الواحدة* التي يمكن أن يوجد بها المفتاح، ثم يقرأ تلك الكتلة ويفك ضغطها ويمسحها
  — لذا فإن بحث المساواة هو **قراءة كتلة واحدة**، ومسح النطاق يمشي عبر
  سلسلة الكتل المتجاورة التي يغطيها. (لهذا يكون البحث المفهرس عبر `meshUi`
  في نطاق ميلي ثانية واحدة بينما نفس المطابقة على خاصية غير مفهرسة تفحص
  التسمية بالكامل.)
* يختاره المخطط عبر `NodeScan::RangeEq` / `RangeRange`؛ أما المسند
  (predicate) غير المفهرس فيتراجع إلى مسح التسمية أو الفحص الكامل، مع إعادة
  فحص المُنفِّذ لكل مسند في كلتا الحالتين.

### البحث المتجهي (Vamana + PQ) — cosine وL2 وdot، قراءة *و* كتابة

يعمل بحث الجيران الأقرب المتجهي KNN (`db.idx.vector.queryNodes`) عبر فهارس
**cosine أو L2 أو dot-product (MIPS)**. يُبنى الفهرس الأساسي دون اتصال (offline)
بمسارَي تنفيذ، يُختار لكل فهرس عبر `--ann-threshold` (الافتراضي 50 000 متجه):

* **أقل من العتبة — القوة العمياء (brute force).** المتجهات الكاملة `f32`
  موجودة في `vectors.f32.blk`؛ يمسح الاستعلام مجموعة الفهرس ويحسب المسافة
  الدقيقة بمقياس الفهرس. بسيط ودقيق؛ مناسب عندما تكون مجموعة المتجهات صغيرة.
* **عند العتبة أو أعلى — Vamana + PQ**، مسار الشبكة العصبية التقريبية (ANN)
  الأصلي للقرص الذي يبقي الذاكرة المقيمة محدودة بغض النظر عن عدد المتجهات:
  * **[Vamana](https://arxiv.org/pdf/2401.11324)** هو فهرس الرسم البياني من
    سلالة أعمال DiskANN: رسم بياني واحد للتقارب تُشذَّب حوافه (عامل
    `--vamana-r` لدرجة الخروج و`--vamana-alpha` للحواف الطويلة) بحيث *بحث
    الشعاع الجشع (greedy beam search)* — يبدأ من النقطة الوسطى (medoid)،
    ويقفز مرارًا نحو الاستعلام، مُبقيًا قائمة مرشحين بعرض
    `vectorQuery.beamWidth` — يصل إلى الجيران الحقيقيين للعقدة في قفزات
    قليلة، أي **قراءات كتل عشوائية قليلة لكل استعلام**. تُستدعى كتل الرسم
    البياني (`vector/<label>.<prop>.vamana`) إلى الذاكرة عبر ذاكرة التخزين
    المؤقت للمتجهات، ولا تُحتفظ بها كاملة.
  * **[التكميم المنتج (Product quantisation اختصارًا PQ)](https://medium.com/aiguys/product-quantization-k-nn-for-big-datasets-12431d764c4e)**
    يضغط كل متجه إلى كود قصير (`--pq-subspaces` × `--pq-bits`): تُقسَّم
    الأبعاد إلى فضاءات فرعية، ويُجمَّع كل منها بشكل مستقل بخوارزمية k-means،
    ويُخزَّن المتجه كمجموعة معرّفات أقرب المراكز. هذه الأكواد
    (`vector/<label>.<prop>.pq`) صغيرة بما يكفي لإبقائها **مقيمة**، لذا
    يسجّل بحث الشعاع المرشحين من RAM ولا تُقرأ من القرص إلا المتجهات الكاملة
    القليلة المختارة. مجموعة PQ المقيمة هذه هي ما تثبّته ذاكرة
    `cache.vectorCacheBytes`.

**التمثيلات (embeddings) القابلة للكتابة — سلم كتابة المتجهات (بأسلوب [FreshDiskANN](https://arxiv.org/abs/2105.09613)).**
التمثيل المفهرس قيمة قابلة للكتابة من الدرجة الأولى. `SET n.embedding = vecf32([…])`
(و`REMOVE`) يقع في دلتا الكتابة ويكون **مشاهدًا فورًا في بحث KNN بترتيب دقيق**،
ثم يبقى بعد تفريغ المقطع والدمج والدمج النهائي. يدمج الاستعلام حتى ثلاثة مستويات
— الفهرس الأساسي المغلق، وفهرس مغلق لكل مقطع، و**فهرس RW** في الذاكرة (Vamana
حي قابل للتغيير فوق دلتا الكتابة) — لذا تظل زمن الوصول ثابتًا مع تراكم الكتابات
بدل أن ينمو مع عدد الكتابات المعلقة. الحذف يترك *ثقبًا*: تتوقف العقدة عن الظهور
في النتائج لكنها تبقى نقطة عبور ملاحية حتى تقوم **دمج الحذف** الخلفي باستئصالها
من الرسم البياني، لذا تتوقف عمليات الحذف عن كلفة قراءة/كتابة في الاستعلام.
ولأن الرسم البياني على القرص يعنون جيرانه بموضع التخطيط لا بمعرّف العقدة، فإن
`CALL slater.consolidate()` يحمل Vamana **بالإشارة (by reference)** — مرتبطًا
صلبًا (hard-linked) ومطابقًا بايتًا ببايت — ويعيد كتابة عمود معرّفات صغير فقط،
طاويًا كتابات المتجهات في الأساس **دون** إعادة بناء رسم بياني O(N·R·L).
الأرقام المقاسة، مع تحفظاتها، موجودة في [تقرير الأداء](https://github.com/hikari-systems/slater/blob/HEAD/docs/PERF-REPORT.md).

## الواجهات الخلفية للتخزين (نظام الملفات / S3 / GCS)

يُفتح كل ملف من ملفات الصيغة عبر تجريد **`ObjectStore`** بدلًا من `std::fs`
مباشرة، لذا فإن *نفس* تنسيق البايت على القرص — الكتل والفهارس والمانيفست
ومؤشر `current` — يُقدَّم دون تغيير من أي واجهة خلفية؛ يختلف فقط *من أين
تأتي البايتات*، ولا يختلف أبدًا القارئون أو محرك الاستعلام أو فحوص التكامل.
المسار الساخن هو قراءات موضعية (`read_exact_at`)، تترجم إلى `pread` على ملف
محلي وطلب نطاق بايتات HTTP على مخزن كائنات — لا تستخدم Slater أبدًا mmap،
لذا فإن نموذج القراءة الصريح المحدود مطابق في كل مكان.

**ثلاث واجهات خلفية من الدرجة الأولى**، تُختار عبر `dataBackend.kind`. نظام
الملفات هو الافتراضي البسيط؛ **أمازون S3 وتخزين جوجل السحابي (GCS) واجهتان
خلفيتان متساويتان ومدعومتان بالكامل لمخازن الكائنات** — الصورة المنشورة تتضمن
كلتيهما مدمجتين، لذا فكل منهما مجرد إعداد، والصيغة التي بُنيت مرة واحدة يمكن
تقديمها من أي منهما (حتى مع الترحيل `fs` → S3 → GCS) دون إعادة بناء.

| `dataBackend.kind` | قراءة موضعية | التكامل عند الفتح | بيانات الاعتماد |
| --- | --- | --- | --- |
| `fs` *(الافتراضي)* | `pread` | إعادة تجزئة BLAKE3 كاملة لكل ملف | — |
| `s3` | HTTP `Range` GET | **SHA-256** من الخادم عبر `HEAD` (→ إعادة تجزئة الجسم BLAKE3 إذا غاب) | مفاتيح الإعداد أو سلسلة AWS أو دور IAM |
| `gcs` | قراءة نطاق HTTP | **CRC32C** من الخادم عبر `get_object` (→ إعادة تجزئة الجسم BLAKE3 إذا غاب) | ADC / Workload Identity أو JSON لحساب الخدمة |

يتحقق مخزنا الكائنات من التكامل من **المجموع الاختباري الذي يحسبه المخزن
ويحتفظ به بالفعل**، ويُجلب كبيانات وصفية للكائن: يرسل `slater-build` المجموع
الاختباري عند الرفع (يتحقق المخزن من البايتات مقارنةً به ويخزنه)، ويقرؤه الخادم
عند الفتح ويقارنه بالمانيفست — طلب بيانات وصفية واحد لكل ملف، دون تنزيل الجسم.
إنه بمستوى المحتوى ومطابق في المبدأ عبر S3 (SHA-256) وGCS (CRC32C). عندما
يحمل كائن **لا** مجموع اختباري مخزّن على الخادم (مثلًا نُسخ خارج النطاق، أو
رُفع بإعداد افتراضي مختلف)، يعيد الخادم **تجزئة جسم الكائن مقابل BLAKE3 في
المانيفست** بدلًا من الثقة بطول البايتات — لا يُخفَّض فحص التكامل المطلوب أبدًا
بصمت إلى مقارنة أحجام. الصيغ المنشورة عبر Slater تحمل دائمًا المجموع الاختباري،
لذا تبقى على مسار البيانات الوصفية الرخيص.

ما يفحصه هذا العمود، على كل واجهة خلفية، هو أن الملفات **تطابق المانيفست**.
أما إمكانية الوثوق بالمانيفست نفسه فسؤال منفصل، والمفتاح الرئيسي هو الذي يجيب
عليه: عند تكوين مفتاح، يحمل المانيفست MAC مفتاحيًا (keyed MAC) يتحقق منه
الخادم قبل الوثوق بأي حقل (بما في ذلك هذه التجزئات)، لذا يُرفض مانيفست أُعيدت
كتابته لوصف ملفات عُبث بها؛ وبدون مفتاح تظل المقارنة بلا مفتاح في كل مكان،
ومن يستطيع الكتابة إلى دليل البيانات يمكنه إعادة كتابة ملف ومانيفست معًا.
انظر [ما يعنيه التكامل في كل إعداد](https://github.com/hikari-systems/slater/blob/HEAD/THREAT_MODEL.md#what-integrity-means-in-each-configuration).
يمكن إيقاف الفحص نفسه عبر `dataBackend.verifyIntegrity: false`، وهو ما
يُستبدل بفتح أسرع.

### نظام الملفات (`fs`)

الافتراضي، وجذره عند `dataBackend.fs.dir`. الخيار الصحيح لمعظم النشرات: صيغة
على SSD محلي (أو نقطة تحميل NFS/EBS) تُقدَّم للقراءة فقط. التكامل هو إعادة
تجزئة BLAKE3 كاملة لكل ملف عند الفتح.

### أمازون S3 (`s3`)

دلو S3 أو دلو متوافق مع S3 (AWS أو MinIO أو localstack). تأتي بيانات الاعتماد
**أولًا** من الإعداد (`dataBackend.s3.awsAccessKey` / `awsSecretKey`، إضافة
إلى `awsSessionToken` لبيانات اعتماد STS المؤقتة)، وتتراجع إلى سلسلة AWS
القياسية (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` كمتغيرات بيئة، أو ملف
التعريف المشترك، أو دور المثيل/IRSA) عندما تُترك فارغة.```sh
# serve from S3 (env-var form; see the config table for every key)
dataBackend__kind=s3
dataBackend__s3__bucket=slater
dataBackend__s3__region=eu-west-2
dataBackend__s3__awsAccessKey=…        # omit to use the AWS chain / instance role
dataBackend__s3__awsSecretKey=…
# S3-compatible (e.g. MinIO): also set
dataBackend__s3__endpoint=http://minio:9000
dataBackend__s3__pathStyle=true        # required by most S3-compatible servers
# publish a generation into the bucket (remote `current` pointer written last)
slater-build --input people.cypher --graph people --data-dir /data \
  --publish-s3-bucket slater --publish-s3-region eu-west-2 --publish-s3-prefix prod
#   MinIO: add  --publish-s3-endpoint http://localhost:9000 --publish-s3-path-style

Google Cloud Storage (gcs)

دلو GCS يتم الوصول إليه عبر JSON API. التخويل أصلي في GCP: بشكل افتراضي، يحلّ بيانات الاعتماد الافتراضية للتطبيق (Application Default Credentials) — GKE Workload Identity، أو خادم بيانات GCE الوصفية، أو مفتاح gcloud / GOOGLE_APPLICATION_CREDENTIALS. اضبط dataBackend.gcs.credentialsPath (ملف مفتاح JSON لحساب الخدمة) أو credentialsJson بشكل مباشر لمفتاح صريح. يشير dataBackend.gcs.endpoint إلى محاكي fake-gcs-server، وdataBackend.gcs.anonymous=true يتيح الوصول غير المصادَق لذلك المحاكي فقط — أبدًا تجاه GCS الحقيقي.```sh

serve from GCS (env-var form; see the config table for every key)

dataBackend__kind=gcs dataBackend__gcs__bucket=slater dataBackend__gcs__prefix=prod dataBackend__gcs__credentialsPath=/secrets/sa.json # omit for ADC / Workload Identity

```sh
# publish a generation into the bucket (remote `current` pointer written last)
slater-build --input people.cypher --graph people --data-dir /data \
  --publish-gcs-bucket slater --publish-gcs-prefix prod
#   explicit key: add  --publish-gcs-credentials /secrets/sa.json

في جميع الحالات، يكتب slater-build الجيل النهائي إلى --data-dir أولاً (منطقة التدريج المحلية الخاصة به) وبالإضافة إلى ذلك يرفعه إلى bucket؛ يُكتب مؤشر current البعيد أخيرًا، لذلك لا ترى أي عقدة خدمة جيلًا منشورًا بشكل نصف مكتمل.

متى تستخدم مخزن كائنات (S3 أو GCS)

استخدم s3 أو gcs عندما تريد أجيالًا في مخزن كائنات مركزي ودائم بدلًا من تخزينها على قرص العقدة — عادةً: انشر مرة واحدة ووزّع على العديد من نسخ الخادم المكررة عديمة الحالة وعديمة الأقراص التي تقرأ جميعها من نفس bucket؛ أو افصل مضيف البناء عن مضيفي الخدمة؛ أو اعتمد على متانة المخزن/إصداراته/دورة حياته بدلًا من إدارة وحدات التخزين. المقابل هو زمن الوصول: الكتلة الباردة تتطلب رحلة شبكة ذهابًا وإيابًا (~10–50 ms) بدلًا من قراءة محلية (~0.1 ms). يخفي Slater معظم ذلك عبر ذاكرة التخزين المؤقت للكتل في الذاكرة، والقراءة المسبقة المتزامنة، و ذاكرة التخزين المؤقت على القرص الاختيارية أدناه. إذا كانت أجيالك موجودة بالفعل على تخزين محلي سريع ولا تحتاج نموذج bucket المركزي، فإن fs أبسط وأسرع.

ذاكرة تخزين مؤقتة للكتل على القرص المحلي (الطبقة الثانية لمخزن الكائنات)

ذاكرة BlockCache في الذاكرة صغيرة عمدًا (ضمان RSS المحدود هو الميزة الرئيسية)، لذلك على مجموعة عمل أكبر من RAM سيتم إعادة جلب الكتل نفسها من مخزن الكائنات عند كل إخراج. تعمل طبقة تخزين مؤقت ثانية اختيارية على SSD محلي على إصلاح ذلك: الكتلة المُخرجة من RAM تُقدَّم من القرص المحلي (~0.1 ms) بدلًا من GET جديدة من مخزن الكائنات، فتبقى حية رغم الإخراج من الذاكرة وتقلل عدد/تكلفة طلبات مخزن الكائنات — مما يقرب عقدة مدعومة بمخزن كائنات من أداء نظام الملفات المحلي بعد أن تسخن. وهي اختيارية لكل من s3 و gcs، وتُفعَّل بضبط dataBackend.<s3|gcs>.diskCacheBytes > 0 و diskCacheDir قابل للكتابة.

  • يخزّن مؤقتًا البايتات المغلقة تمامًا كما جُلبت — مضغوطة بالفعل، و(للأجيال ذات --encrypt) ما تزال مغلقة بـ AEAD — تحت فك التشفير/فك الضغط. لا تحتفظ طبقة التخزين المؤقت أبدًا بمفتاح التشفير ولا تعيد التشفير، لذلك يتم الحفاظ على حالة التخزين مجانًا: يصل الجيل المشفر إلى القرص ما يزال مغلقًا.
  • الكتابة كتابة خلفية (write-behind): عند عدم التطابق، تُعاد البايتات الجلوبة إلى الاستعلام فورًا، ثم يقوم خيط في الخلفية بكتابة القرص وتقليم LRU، لذلك لا يُحظَر مسار الاستعلام أبدًا على الإدخال/الإخراج القرصي. يُبقي الإخراج ذاكرة التخزين المؤقت ضمن ميزانية بايتاتها؛ ويُصلح المجموع الاختباري لكل ملف والذي يُتحقق منه عند كل قراءة ملفًا تالفًا من ذاكرة التخزين المؤقت بحيث يتحول إلى عدم تطابق (→ إعادة جلب من مخزن الكائنات).
  • يجب أن يشير diskCacheDir إلى وحدة تخزين حقيقية قابلة للكتابة — أبدًا tmpfs (tmpfs هو RAM وسيلغي ضمان RSS المحدود). يكلف الفهرس داخل الذاكرة الذي يتتبعه القليل من RAM (عشرات البايتات لكل كتلة مخزنة مؤقتًا تقريبًا)، وهو محسوب ضمن سقف RSS لديك — اجعل حجم الدليل أكبر بكثير ≫ من ذاكرة التخزين المؤقت للكتل داخل الذاكرة.
  • التكلفة الأخرى لذاكرة RAM في هذه الطبقة هي قائمة انتظار الكتابة الخلفية، التي تنظم الكتل في طريقها إلى القرص. وهي محدودة بـ blockCacheBytes / 8 (مع أرضية هي diskCacheBytes) — 8 MiB عند الافتراضي — وتتخلص من الكتل بدلًا من أن تنمو، لذلك لا يمكن لفحص بارد أن يضخمها؛ الكتلة المتخلص منها تُعاد جلبها ببساطة عند عدم تطابقها التالي. لا تحتاج إلى أي إعداد: فهي تتدرج مع blockCacheBytes، وبالتالي لا تضيف طبقة القرص رقمًا جديدًا إلى ميزانية RSS بعد فهرسها.

نقاط التحميل

تعمل نسخة القراءة المكررة بنظام ملفات جذر للقراءة فقط ومستخدم غير جذر (appuser:1000) — كل ما تحتاجه يُحمَّل للقراءة فقط. يحتاج الكاتب (delta.enabled) بالإضافة إلى ذلك وحدة تخزين دائمة واحدة قابلة للكتابة من أجل WAL الخاص به.

المسارالغرضملاحظات
/dataأجيال الرسم البياني (<graph>/<uuid>/… + current).للقراءة فقط للنسخ المكررة؛ يُنتَج بواسطة slater-build. قد يكون على تخزين بعيد/شبكي (مثل NFS)، لذا لا يُفترض أن تكون القراءات بسرعات SSD محلية سريعة.
/sandboxطبقة إعدادات متراكبة لكل بيئة + أسرار.يتم دمج /sandbox/config.json بعمق فوق config.json المدمج؛ يحمل أيضًا acl.json ومواد TLS PEM وملف مفتاح التخزين.
/tmp, /runمساحة عمل مؤقتة (tmpfs).لا تكتب نسخة القراءة المكررة على القرص افتراضيًا.
(writer) delta.walDirسجل الكتابة المسبقة + مقاطع دلتا L0، عند delta.enabled.قابل للكتابة، ووحدة تخزين حقيقية دائمة — أبدًا tmpfs (فهي أرضية المتانة). المسار النسبي يُحل تحت دليل البيانات؛ امنح الكاتب وحدة تخزين دائمة خاصة به هنا.
(optional) disk cacheذاكرة التخزين المؤقت للكتل على القرص المحلي، عندما dataBackend.s3.diskCacheBytes / dataBackend.gcs.diskCacheBytes > 0.قابل للكتابة ووحدة تخزين حقيقية — ليست tmpfs. تُستخدم بواسطة خلفيتي s3 و gcs؛ انظر خلفيات التخزين.

البيئة / الإعدادات

يتم تحميل الإعدادات عبر أداة التحميل الطبقية القياسية: ملف config.json المدمج، ثم /sandbox/config.json المدمج بعمق فوقه، ثم تجاوزات البيئة KEY__sub (شرطتان سفليتان للتداخل؛ المفاتيح تطابق إعدادات camelCase).

كل مقبض إعداد — مفتاحه camelCase، تجاوز البيئة KEY__sub، قيمته الافتراضية، وما يفعله — مُجدول في مرجع الإعدادات. أكثر المقابض التي تُضبَّط عادةً هي ميزانيات التخزين المؤقت (cache.*)، وحرّاسات الاستعلام (query.*)، وسقوف الاتصال (server.*)، وخلفية التخزين (dataBackend.*)، والطبقة القابلة للكتابة (delta.*).

الذاكرة المقيمة تتبع blockCacheBytes + vectorCacheBytes + resultCacheBytes ضمن عبء علوي محدود لكل إدخال ومن جانب المخصّص (allocator) — كل تجمّع يزن محتوياته الخاصة (السلاسل والحاويات حسب السعة المخصصة) ويُخرج منها للبقاء ضمن الميزانية، لكن المسك الدفتري لكل إدخال وتقريب فئات الحجم للمخصّص يأتيان فوق الرقم الذي تضبطه — بالإضافة إلى عبء ثابت صغير (وحتى degreeColumnBytes لعمود الدرجة lazy، بمجرد تنفيذ المسار السريع لمجموع الدرجات count(endpoint)). وهو مستقل عن حجم الرسم البياني — هذا هو الضمان الرئيسي، ويُختبر عبر اختبار التكامل rss_stays_bounded_under_sustained_knn_load، الذي يُبقي نمو RSS بين الذروة والدفء داخل الميزانيات المجمّعة بشكل جيد. تعيش مخازن كل اتصال المؤقتة خارج ميزانيات التخزين المؤقت، لذلك يصمد الضمان تحت الحمل العدائي فقط لأن server.maxConnections يحد من عددها الموجود في وقت واحد.

الوضعية الشبكية

Slater هو مقبض نسخة قراءة مكررة؛ التحكم الأساسي في أمان الاتصال هو الشبكة، وليس البرنامج الثنائي. اربطه بواجهة خاصة، وقيّد نطاقات المصدر على طبقة الشبكة (مجموعات الأمان / NetworkPolicy)، وإذا كان يواجه أي شيء سوى عملاء موثوقين — ضع أمامه وكيل L4 يحد الاتصالات (HAProxy maxconn + stick-table لكل مصدر، أو nftables connlimit + hashlimit). يقع ذلك قبل أن يُمرَّر واصف الملف على الإطلاق إلى العملية، لذلك فهو الحد الأكثر متانة.

الحدود داخل البرنامج أعلاه (maxConnections, maxPreAuthConnections, maxConnectionsPerIp, سقوف البايت التفاضلية، و loginTimeoutMs) هي دفاع متعمق: مفعّلة افتراضيًا وسخية بحيث تكون غير مرئية لمجتمع عملاء شرعي، لكنها تجعل ضمان RSS المحدود يصمد حتى عندما يُنسى الوكيل. انظر docs/HARDENING.md للوضعية الدفاعية الكاملة، وTHREAT_MODEL.md / SECURITY_WORKLIST.md للتفاصيل المعيارية.

حارس الأجيال

يستقصي Slater مؤشر current لكل رسم بياني كل generationPollMs (استقصاء، وليس inotify — قد يكون دليل البيانات تخزينًا بعيدًا/شبكيًا مثل NFS، حيث تكون أحداث تغيير نظام الملفات غير موثوقة). عندما يتغير:

  • reloadStrategy=exit (الافتراضي): يسجّل الخادم خطأً فادحًا (fatal) ويخرج برمز غير صفري ليعيده منسّق النشر (orchestrator) نظيفًا على الجيل الجديد.
  • reloadStrategy=swap: يفتح الخادم الجيل الجديد ويتحقق منه (نفس حارس تجزئة المحتوى عند الإقلاع)، ويستبدله بشكل ذري، ويترك الاستعلامات الجارية تنتهي على الجيل القديم. يتم رفض الصورة الجديدة التالفة/غير المكتملة ويستمر الجيل القديم في الخدمة.

ACL

يُعيّن acl.json المستخدمين إلى تجزئات كلمة مرور argon2id ومنح read / write لكل رسم بياني. قم بإنشاء تجزئة (لا تخزن نصًا صريحًا أبدًا) باستخدام:```sh slater hash-password 's3cret' # prints a $argon2id$… string for acl.json

ملف بدء `acl.json` مُضمَّن في جذر المستودع؛ ويكون شكله كالتالي:```json
{
  "users": {
    "reporting": {
      "passwordArgon2id": "$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>",
      "grants": {
        "people": ["read"],
        "products": ["read", "write"]
      }
    }
  }
}
  • users — إدخال واحد لكل تسجيل دخول، مُفهرس باسم المستخدم.

  • passwordArgon2id — سلسلة $argon2id$… الناتجة من slater hash-password (لا تكون نصًا صريحًا أبدًا؛ فالملف نفسه JSON عادي ويقع على تخزين مشترك).

  • grants — قوائم صلاحيات لكل رسم بياني (graph). هناك صلاحيتان ذات معنى:

    • read — الاستعلام عن الرسم البياني. أي رسم بياني غير موجود في صلاحيات المستخدم يكون غير مرئي له.
    • write — تعديل الرسم البياني عبر الطبقة القابلة للكتابة (delta.enabled): عبارات MERGE / SET / DELETE وCALL slater.consolidate().

    هما مستقلّان: صلاحية read لا تمنح أي وصول للكتابة. لذلك تشغيل الطبقة القابلة للكتابة لا يمكن أن يحوّل القرّاء الحاليين إلى كتّاب. الكاتب يحتاج كلاهما — ["read", "write"] — لأن حلّ مفتاح العمل (business key) لكتابته هو قراءة. سلاسل الصلاحيات غير المعروفة يتم تجاهلها (لا تمنح شيئًا).

قم بتثبيته للقراءة فقط في المسار المحدد بواسطة aclPath (الافتراضي /config/acl.json). يعيد الخادم تحميله عند كل تبديل ساخن للأجيال (generation hot-swap)، ويتم إعادة التحقق من طابع ACL الثابت (at-rest ACL stamp) عند كل إعادة تحميل (انظر requireAclStamp).

فحص الصحة

تعمل ثنائية slater أيضًا كأداة فحص بقاء خاصة بها: slater healthcheck [host] [port] يؤدّي مصافحة Bolt (وليس طلب HTTP) مع الخادم ويخرج بـ 0 إذا نجح في التفاوض على إصدار بروتوكول، وبـ 1 خلاف ذلك — مع افتراض localhost ومنفذ Bolt المكوّن. هذا ما يشغّله HEALTHCHECK في الحاوية، بحيث يرى منظّمو الحاويات خادمًا جاهزًا فعليًا لـ Bolt، وليس مجرد مقبس مفتوح:```sh slater healthcheck localhost 7687 # exit 0 = healthy docker exec slater /app/slater healthcheck # inside the container

## استعلام لمرة واحدة

للسكربتات، وفحوصات CI، والاستعلامات السريعة، يقوم `slater query` بتركيب الجيل الحالي للرسم البياني، وينفّذ استعلام Cypher واحدًا للقراءة فقط داخل العملية، ويطبع النتيجة ككائن JSON، ثم يخرج — بدون خادم، وبدون اتصال Bolt. وهو يحترم نفس إعدادات الخادم (نظام التخزين الخلفي، مفتاح التشفير، ميزانيات الاستعلام):```sh
# GRAPH defaults to `defaultGraph`. Without -q, normal datestamped logging
# (config, "opened generation", …) is written to stdout alongside the result.
slater query mygraph 'MATCH (n) RETURN count(n) AS c'

# -q/--quiet ⇒ logging suppressed, so stdout is *only* the compact result JSON
slater query mygraph -q 'MATCH (c:Company) RETURN c.ticker AS t LIMIT 3' | jq
# {"columns":["t"],"rows":[["AUPH"],["KYMR"],["MREO"]]}

تُوسَّع العُقد والعلاقات لتشمل تسمياتها/أنواعها وخصائصها. استخدم -q عندما تريد مخرجات قابلة للتحليل آليًا (يكون JSON الناتج هو الشيء الوحيد على stdout)؛ واحذفها لتشغيل موجَّه للمشغِّل مع السجلات. بدون -q، يُسجَّل ملخص يقتصر على المقاييس بعد كل تشغيل — على سبيل المثال.```text INFO query executed cost=2389 resultCount=10 execMs=441 limitRowCount=10

حاملًا الاستعلام `cost` (العناصر التي تُحتسب)، `resultCount`، `execMs`، و
`limitRowCount` (فقط عندما يحدد الاستعلام `LIMIT`) — لا نص الاستعلام أبدًا
ولا أي قيمة ناتجة. حالة الخروج هي `0` عند النجاح، و`1` عند خطأ في التحليل/الفتح/التنفيذ
(رسالة على stderr).

## تصدير رسم بياني (`slater dump`)

`slater dump` يصدّر رسمًا بيانيًا من خادم **قيد التشغيل** كـ Cypher بصيغة `MERGE`
بمفاتيح الأعمال — نفس اللهجة التي يستوعبها `slater-build` — بحيث يمكن للرسم البياني
أن يقوم برحلة ذهاب وعودة (dump → `slater-build` → جيل جديد) للترحيل أو النسخ
الاحتياطي النصي. بخلاف `slater query`، فإنه يتصل عبر **Bolt**، ويوثّق هويته،
ويحترم صلاحيات الوصول (ACLs) لكل رسم بياني، لذا لا يحتاج إلى وصول إلى قرص الخادم.
تُقرأ كلمة المرور من `SLATER_DUMP_PASSWORD` أو stdin (وليست أبدًا خيارًا في
سطر الأوامر، لإبقائها خارج `ps`/history).```sh
# List the graphs the authenticated user may read.
SLATER_DUMP_PASSWORD=pw slater dump --list -u reporting

# Dump a graph to a file (identity keys inferred from range indexes).
SLATER_DUMP_PASSWORD=pw slater dump people -u reporting -o people.cypher

# Rebuild it into a fresh generation.
slater-build --input people.cypher --graph people --data-dir ./data

مفتاح الهوية لكل تسمية هو الخاصية التي يحملها فهرس نطاقها؛ يمكن تجاوزه باستخدام --key Label=prop (قابل للتكرار) أو --pk <field> عام. يتم إصدار DDL الخاص بـ CREATE INDEX أولاً بحيث تعيد عملية إعادة البناء إنشاء الفهارس. العقدة متعددة التسميات تحتفظ بكل تسمية — ويتم إصدارها بصيغة MERGE (n:Ident:Other {key: v})، مع وضع تسمية الهوية (التسمية التي توفّر مفتاح العمل) أولاً وبقية التسميات مرتبة؛ وتُستخدم تسمية الهوية وحدها كمفتاح لعملية الدمج، لذا تُكتب التسميات اللاحقة على العقدة دون إنشاء عقدة أخرى. التسميات وأنواع العلاقات ومفاتيح الخصائص التي تحتوي على أحرف خاصة تُحاط بعلامات backtick عند الإصدار، بحيث تُستعاد الأسماء غير المألوفة بدقة ولا يمكنها حقن Cypher في إعادة البناء. المتجهات (والقيم الأخرى التي لا تملك تمثيلًا حرفيًا في Cypher) لا يمكن تضمينها في تفريغ MERGE، ويتم إسقاطها مع تحذير على stderr. حالة الخروج تكون 0 عند النجاح و1 عند الخطأ.

مثال عملي

شرح كامل قابل للتشغيل — لإنشاء رسم بياني، وتشغيله، والاتصال به عبر برامج تشغيل neo4j الخاصة بـ JavaScript وPython، والكتابة إليه — موجود في صفحتي بدء سريع و كتابة البيانات من الدليل، باستخدام الرسم البياني النموذجي المرفق في docs/manual/examples/.

التطوير```sh

export PATH="$HOME/.cargo/bin:$PATH" cargo build cargo test # unit + the bounded-RSS headline integration test cargo clippy --all-targets -- -D warnings cargo fmt --all -- --check

### خلفيات تخزين الكائنات هي ميزات اختيارية في cargo

مجرد `cargo build` ينتج ملفًا ثنائيًا **لنظام الملفات فقط** — الخلفيات `s3` و`gcs`
مقيدة خلف ميزات cargo بحيث يظل البناء الافتراضي صغيرًا (بدون AWS
أو Google SDK، وبدون وقت تشغيل async). فعّل ما تحتاجه في **كلٍّ** من `slater`
(serve) و`slater-build` (publish):```sh
# S3 only / GCS only / both
cargo build -p slater -p slater-build --features s3
cargo build -p slater -p slater-build --features gcs
cargo build -p slater -p slater-build --features s3,gcs

Each crate exposes matching s3 / gcs features that forward to graph-format/{s3,gcs}. Requesting a backend at runtime (dataBackend.kind=s3|gcs, or slater-build --publish-{s3,gcs}-*) without its feature compiled in fails fast with a clear "built without the … feature" error. The published Docker image enables both (Dockerfile CARGO_FEATURES), so prebuilt images need no extra flags — this only matters when building from source. The integration tests are likewise gated: --features s3 --test s3_minio, --features gcs --test gcs_emulator (a fake-gcs-server), and --features gcs --test gcs_real (real GCS via ADC); each skips unless its SLATER_* env vars are set.

See docs/PLAN.md, docs/PROGRESS.md and docs/DECISIONS.md for the design, the milestone ledger, and the decision log.

الأداء

حتى ستة محركات، ومجموعة اختبارات ذات عميل واحد، ورسوم بيانية من رسم تجريبي صغير بعقدة 62k إلى Wikidata بعقد 91.6M / حواف 1.5B. يُقاس كل محرك بمعزل عن الآخرين (تُوقَف كل الحاويات الأخرى — فقيمة RSS وزمن الاستجابة هما بصمته الخاصة). جداول زمن الاستجابة أدناه أُعيد قياسها على Slater 0.21.0 (البناء القابل للكتابة): الرسوم الصغيرة/المتوسطة (MeSH، EU-AI-Act) قِيسَت حديثًا، ورسم 91.6M كمرور نفس-الجهاز، مرساة-مشتركة جديد بين slater وNeo4j (انظر ذلك الجدول). أما أرقام الذاكرة المقيمة فمحمولة من المرور السابق (قِيست عبر cgroup للحاوية؛ مسار القراءة مطابق بايتًا-ببايت مع طبقة الكتابة خاملة). أرقام المحركات الأخرى هي من تشغيل المحركات المتقاطع المُثبَت (إصداراتها وأداؤها لم يتغيرا). جميع الأرقام هي وسيطات (مللي ثانية) أو ذروة الذاكرة المقيمة (MiB). الأقل أفضل في كل مكان؛ الخط العريض = الأفضل في الصف. شُغِّل slater على خلفيته لنظام الملفات المحلي (fs)؛ أما خلفيتا S3 وGCS فتستبدلان زمن قراءة المحلي بجولات تخزين الكائنات (يخففها التخزين المؤقت في الذاكرة وطبقة ذاكرة التخزين المؤقت المحلية الاختيارية على القرص)، لذا تصف هذه الأرقام المحرك نفسه، لا نشر تخزين شبكي.

المحركالفئةحد الذاكرة
slaterمدعوم بالقرص، مُقسَّم إلى صفحاتquery.maxIntermediate يحد من مجموعة العمل تلقائيًا
Neo4j 5مدعوم بالقرص، JVMكومة ~2 GiB + ذاكرة خارج الكومة، مُلتزَم بها بغض النظر عن الاستعلام
Memgraph · FalkorDBفي الذاكرةالرسم البياني بأكمله مقيم في RAM
ArcadeDBفي الذاكرة، JVMالرسم البياني كله مقيم؛ الأثقل
LadybugDBمضمّن، عموديتجمّع مخازن مؤقتة يدوي يجب أن يتجاوز الاستعلام

المحركات الثلاثة التي تقسّم من القرص — slater وNeo4j 5 وLadybugDB — تُحمِّل الرسوم البيانية الخمسة كلها. أما الثلاثي في الذاكرة (Memgraph · FalkorDB · ArcadeDB) فلا يمكنه استيعاب رسم الحواف 1.5B إطلاقًا (يتطلب ~64–128 GiB مقيمة)، ومستورد ArcadeDB لا يُكمله أيضًا.

الذاكرة المقيمة (MiB) — محدودة بينما ينمو الرسم البياني ~1,500×

كل رقم هو ذاكرة عمل مُلتزَم بها — ما لا يستطيع نظام التشغيل استرداده. كل محرك باستثناء slater يحتفظ برسمه البياني في ذاكرة مجهولة مُلتزَم بها (كومة خاصة، أو ذاكرة تخزين مؤقت للصفحات خارج الكومة في Neo4j، أو تجمّع مخازن مؤقتة)، لذا فإن ذروة RSS فيه هي بصمته الملتزَم بها. أما slater وحده فيخدم من ذاكرة تخزين مؤقت للصفحات قابلة للاسترداد في نظام التشغيل لمخزنه على القرص، لذا فإن رقمه هو مجموعة العمل المجهولة؛ ذاكرة التخزين المؤقت للصفحات الخاصة بالمخزن (قابلة للطرد عند الضغط — ويواصل slater الخدمة) مستبعدة، وتُعرض كـإجمالي بين قوسين لرسم 91.6M. الخط العريض = الأدنى.

رسم بياني (عقد / حواف)slaterNeo4j 5MemgraphFalkorDBArcadeDBLadybugDB
pole — 62k / 106k117461141401,556198
MeSH — 341k / 469k631,0833584551,631121
EU-AI-Act — 21k / 45k (+55 MiB متجهات)997292293121,948286
Wikidata — 91.6M / 1.5B584 (4,595 إجمالي)~2,900غير قابل للتحميلغير قابل للتحميلغير قابل للتحميل~652 †

slater هو الأدنى في كل مقياس وينمو ~50× بينما ينمو الرسم البياني ~1,500× — بصمته تتبع مجموعة عمل الاستعلام، لا الرسم البياني (خامل ~16–71 MiB طوال الوقت). الثلاثي في الذاكرة ينمو بشكل خطي تقريبًا ولا يستطيع تحميل رسم 1.5B؛ وNeo4j يلتزم بكومة ~2 GiB بغض النظر عن الاستعلام. († LadybugDB على الأشكال المحدودة فقط — فعبوراته (hub / var-length / shortestPath) عند حواف 1.5B تتطلب رفع تجمّع القراءة إلى ≥2 GiB، مقابل سقف maxIntermediate التلقائي في slater.) المدرّجات التكرارية value→count في زمن البناء تضيف ذاكرة مقيمة لا تُذكر — بضع كيلوبايتات لعمود مفهرس منخفض التنوع، وصفر لرسوم المفاتيح الفريدة مثل Wikidata (يتجاوز wikidata_id سقف تنوع المدرج التكراري، فلا يُخزَّن أي منها) — لذا فإن هذه الأرقام لا تتغير بهذه الميزة.

زمن الاستجابة (وسيط ms) — رسم بياني يتسع في RAM (MeSH، 341k / 469k)

الشكلslaterNeo4j 5MemgraphFalkorDBArcadeDBLadybugDB
count(*) لكل العقد0.4115.023.816.482.02.2
عدّ التسمية0.424.220.71.14.44.3
بحث نقطي مفهرس0.433.90.480.480.658.8
عدّ idx-eq0.424.95.02.03812.5
1-hop (مرساة مفهرسة)1.285.81.214.13904.9
2-hop (بدون مرساة)1.405.68.516.74446.4
group-by / count(DISTINCT)0.4547–5163–6431–394115.3
فحص كامل CONTAINS0.435.424.11.716.34.1

يهيمن slater على أشكال البيانات الوصفية / الفهرس / الفحص (count، label، idx-eq، scan — ~0.4 ms، أي 10–200× محركات الخدمة)، والبحث النقطي المفهرس (0.43 ms، وهو يتفوّق الآن بفارق ضئيل على 0.48 ms للثنائي في الذاكرة)، والقفز المتعدد بدون مرساة (2-hop بزمن 1.40 ms عبر فحص نوع العلاقة، الأسرع في المجال)، و— عبر مدرج تكراري value→count في زمن البناء على مفتاح التجميع المفهرس — group-by / count(DISTINCT) للتسمية الكاملة (0.45 ms، متقدمًا على 5.3 ms العمودية في LadybugDB). لا تحتفظ خوادم الذاكرة إلا بـ 1-hop الخام (Memgraph 1.21 ms مقابل 1.28 ms لـslater). (pole 62k/106k يبدو مماثلًا: slater الأسرع وحده في count/scan عند 0.4 ms، و1.3–2.6 ms في القفزات.)

زمن الاستجابة (وسيط ms) — المتجهات (kNN في EU-AI-Act، 15k × 1024-dim)

الشكلslaterNeo4j 5MemgraphFalkorDBLadybugDB
kNN top-10 Concept2.98.61.91.22.8
kNN top-10 Chunk2.45.71.91.53.2

يجيب slater عن kNN بفحص قوة-غاشمة دقيق (هذه المجموعات أقل من عتبة ANN الخاصة به البالغة 50k متجهًا) بينما يستخدم الآخرون HNSW تقريبيًا مقيمًا — لذا فإن نتائج slater دقيقة (استدعاء 1.0). نواة مسافة SIMD + مصفوفة متجهات مقيمة ومعايَرة مسبقًا خفّضت Concept من ~23 → ~2.9 ms وChunk من ~10 → ~2.4 ms، لذا يتفوق slater الآن على Neo4j وLadybugDB ويقع في حدود ~1.4× من Memgraph، ولا يتخلف إلا عن FalkorDB — مع بقائه دقيقًا.

سلم كتابة المتجهات — إدراج / تحديث / حذف دون إعادة بناء

الجداول أعلاه مقارنات قراءة بين المحركات. أما مسار كتابة المتجهات (سلم الكتابة بأسلوب FreshDiskANN فوق قاعدة Vamana الثابتة) فليس له نظير بين المحركات — لا يقوم أي محرك آخر هنا بـANN أصلي على القرص قابل للكتابة — لذا فإن الأرقام أدناه هي معايير مكوّنات لمحرك واحد على بيانات اختبارية تركيبية شبيهة بالتمثيلات المضمّنة (embeddings) (شعاع منخفض الرتبة، بُعد 768، معايير غير متساوية)، مودَعة تحت crates/slater/benches/ وموثقة بالكامل — مع المنهجية وكل التحفظات — في docs/PERF-REPORT.md. يُقاس الاستدعاء دائمًا مقابل قوة-غاشمة دقيقة على المجموعة الحية، لا مقابل فهرسٍ بفهرس. المقياس هنا تمثيلي ولا يُستقرأ إلا حيث يكون القياس خطيًا في الحجم.

الخاصيةالقياسلماذا يهم
زمن استجابة KNN مقابل الكتابات المعلَّقةفهرس RW ~1.5–2 ms، مسطّح حتى 50k معلَّقة؛ الطبقة العلوية بالقوة-الغاشمة قبل الفهرس 1.9 → 115 ms (خطية في الدلتا) — 61× عند 50kزمن استجابة الاستعلام لا يتدهور بينما تتراكم الكتابات بين عمليات الدمج
إدراج تمثيل مضمّن~1.5–2 ms لكل متجه في الفهرس الحيالكتابة تصبح مرئية لـKNN فورًا؛ ميزانية إعادة بناء الدلتا ≈ 2 ms × سقف الدلتا
إدخال/إخراج الحذف عند استدعاء متساوٍأقل بـ2.9× من جلب العقد لكل استعلام عند حذف 67 %، و5.2× عند 80 % (استدعاء ≥ 0.90)الرسم المدمج لا يدفع ضريبة قراءة مقابل المتجهات المحذوفة
الدمج، تبديل خالصO(1).vamana مربوط بوصلة صلبة (hard link) ومطابق بايتًا-ببايت، ولا يُعاد كتابة سوى عمود المعرّفطي كتابات المتجهات في القاعدة يتجاوز إعادة البناء O(N·R·L)
الاستدعاء عبر السلمالمدمج ≥ القاعدة بالنسبة إلى cosine وL2 وdotسلم الكتابة يحافظ على الاستدعاء في كل درجة

الرقم الوحيد الذي يستحق صندوق الأداء المخصص هو إنتاجية إعادة كتابة الدمج في المسار البطيء — فعندما تحمل عملية الدمج حذفًا أو متجهات جديدة بدلًا من تبديل خالص، تكون إعادة ضغط تسلسلية محدودة بـzstd أحادي الخيط والقرص المحلي، لذا فإن MiB/s المطلقة خاصة بالبيئة (يعرض التقرير الشكل ويشرح النطاق البيئي).

زمن الاستجابة (وسيط ms) — رسم بياني ≫ RAM (Wikidata 91.6M / 1.5B)

محركات الذاكرة (Memgraph / FalkorDB / ArcadeDB) لا تستطيع تحميل هذا الرسم إطلاقًا (~64–128 GiB مقيمة). لا يفعل ذلك سوى slater وNeo4j 5. هذا مرور جديد، على نفس الجهاز وفي نفس اليوم، ضد مجموعة مراسٍ ثابتة مشتركة — كل استعلام يضرب نفس العقد على كلا المحركين، لذا فإن المواجهة المباشرة قابلة للمقارنة فعلًا (تجمّع wikidata_id مشترك من مراسٍ متوسطة الدرجة؛ انظر الملاحظة أدناه حول سبب أهمية ذلك). يُعرض slater عند قيمتي fanout (query.maxFanout 1 = الإعداد الافتراضي للإنتاجية، 8 = مؤشر زمن الاستجابة الذي يداخل قراءات الكتل الباردة). الخط العريض = الأفضل في الصف.

الشكلslater (fan 1)slater (fan 8)Neo4j 5
count(*) لكل العقد0.410.413606
بحث نقطي (مفهرس)0.720.496.3
الدرجة (عدّ 1-hop)0.430.446.0
جيران 1-hop9.84.510.1
2-hop372334.5
3-hop322574
var-length *1..2 مميّز985105647

الصورة الصادقة: slater يهيمن على أشكال البيانات الوصفية / الفهرسcount(*) تُخدَم من البيانات الوصفية (0.41 ms مقابل فحص قرصي 3.6 s في Neo4j، أي 8800×)، والبحث النقطي / الدرجة / 3-hop أسرع بـ2–10× — وهو متعادل مع Neo4j في 1–2-hop (fanout 8 يتقدم في القراءات الباردة)، لكنه يخسر var-length *1..2 distinct بشكل حاسم (≈1 s مقابل 47 ms لـNeo4j): توسعة المتغير-الطول المميّزة في slater أبطأ بشكل جوهري هنا، وهو ضعف حقيقي يستحق تحقيقًا خاصًا به. وكل ذلك عند بضع مئات من MB من RSS مقابل كومة ~2 GiB الملتزَم بها في Neo4j.

بشأن المراسي. تعتمد أرقام العبور هذه اعتمادًا كبيرًا على أي العقد تبدأ منها — فالعقدة الواقعة على بُعد صلة واحدة من مركز ضخم في Wikidata ("human"، "country") تمتلك جوار 2-hop بملايين العقد، لذا تتأرجح تكلفة var-length/القفزات بمراتب حجمية حسب اختيار المرساة. النسخة السابقة من هذا الجدول أخذت عينات من "أول N بالفحص" الخاصة بكل محرك، وهو أمرٌ ليس مستقرًا ولا قابلًا للمقارنة؛ هذا المرور يثبّت مجموعة مراسٍ واحدة مشتركة ومحدودة الدرجة لكلا المحركين. (shortestPath مستبعد من هذا المرور — فبين مرساتين عشوائيتين يعتمد على وجود المسار، وتباينه كبير جدًا بحيث لا يُحسب له وسيط ذو معنى.)

count(*) متعدد القفزات — الذاكرة مفصولة عن حجم النتيجة

RETURN count(*) متعدد القفزات غير المحدود يعدّ أثناء التوسعة بدلًا من تجسيد الصفوف المطابقة. نفس مراسي المركز على رسم 91.6M، maxIntermediate=20M:

3-hop count(*) @ 91.6Mfanout=1fanout=8
زمن الاستجابة / ذروة مجموعة العمل554 ms / 0.66 GiB298 ms / 1.9 GiB

العد يحتفظ بصفوف O(1). آلية المحاسبة لم تتغير، لذا فإن عدّ مركز ضخم ما يزال يوقِع maxIntermediate على الحساب (قراءات التجاور)، بشكل محدود كما كان سابقًا.

التوازي لكل استعلام (maxFanout)

رفع query.maxFanout يوزّع قراءات الكتل الباردة المقيدة بالإدخال/الإخراج للاستعلام بالتوازي عبر الأنوية — فهو يساعد الأشكال المقيدة بالقرص ذات مجموعة العمل الباردة الكبيرة، ويكون مسطّحًا على الأشكال الدافئة. على رسم 1.5B: shortestPath ≤6 918 → 608 ms (1.5×، أكبر بحث 6,269 → 2,350 ms، أي 2.7×)؛ عدّ 3-hop 547 → 298 ms. maxFanout=1 هو الافتراضي (موجّه للإنتاجية)؛ أما 8 فهو مؤشر زمن الاستجابة، على حساب ذاكرة عامل مؤقتة أعلى.

أين يتفوق slater / وأين يتخلف

البُعدslaterالأفضل في المجالالحُكم
الذاكرة المقيمة، أي مقياس11–584 MiB (62k → 91.6M)في الذاكرة 1.5–2.7 GiB؛ لا يمكن تحميل 1.5Bslater
count / البيانات الوصفية / الفحص~0.4 msمحركات الخدمة 5–80 msslater (10–200×)
بحث نقطي مفهرس0.43 ms (MeSH)Memgraph · FalkorDB 0.48 msslater (يتفوق بفارق ضئيل على الثنائي في الذاكرة)
قفز متعدد بدون مرساة (صفوف)1.40 ms (MeSH 2-hop)Neo4j 5.6 msslater (فحص نوع العلاقة)
تجميع (group-by / DISTINCT)0.45 msLadybugDB 5 ms (عمودي)slater (مدرج تكراري في زمن البناء)
kNN2.4–2.9 ms (دقيق)FalkorDB 1.2 ms (HNSW)يتفوق على Neo4j/Ladybug؛ ~1.4× خلف Memgraph؛ دقيق
بيانات وصفية / نقطي / درجة / 3-hop عند 91.6M0.4–32 msNeo4j 6–3,600 msslater (2–8800×)
1–2-hop عند 91.6M4.5–23 ms (fan 8)Neo4j 10–35 ms~تعادل
var-length *1..2 مميّز عند 91.6M~1 sNeo4j 47 msNeo4j (نقطة ضعف حقيقية لـslater)
count(*) متعدد القفزات على نطاق واسع0.3–0.6 GiBمحركات الذاكرة تجسّد مجموعة الصفوفslater، محدود

الجداول الكاملة لكل محرك (pole، MeSH، EU-AI-Act + مؤشر RAM↔زمن الاستجابة blockCacheBytes، وWikidata 1M و91.6M) موجودة في perf/cross-engine-hs/README.md؛ أما مرور slater وحده الجديد (قيمتا fanout، كل مجموعة بيانات) فموجود في perf/PERF_CURRENT_STATUS.md.

التزامن والتدهور التدريجي (brown-out) — اختبار الحمل

المعايير أعلاه بعميل واحد. أما المحور المكمّل — السلوك تحت عملاء متزامنين كثيرين — فله منصّة اختبار خاصة به، perf/loadtest/: سائق Locust عبر Bolt مع منسّق يرفع الحمل تدريجيًا، ويقرأ CALL slater.diagnostics()، ويحدد نقطة انحناء السعة، ويسمي المحدِّد (المنهجية الكاملة في docs/LOAD-TESTING.md). أبرز النتائج من تشغيل بذاكرة تخزين مؤقت 256 MiB على رسم Wikidata-1M (جهاز واحد بـ16 نواة):

النتيجةالقياس
يصمد أمام 1000 عميل متزامن، صفر أعطالتبلغ الإنتاجية ذروتها ~2.5k طلب/ثانية؛ تبدأ نقطة انحناء زمن الاستجابة عند ~750 عميلًا (p99 51 → 750 ms) — طوابير تحت تنازع الأنوية، لا سقف صلب (تشغيل واحد، WSL2)
ذاكرة تخزين مؤقت للكتل محدودة وفعّالةمعدل إصابة 100%، صفر عمليات إخلاء، 50 MB مقيمة لمجموعة عمل تتناسب مع التخزين المؤقت
RSS ممسوكة تحت حمل مستمرمُخصِّص jemalloc يمسك RSS عند ~0.6 GB عبر منحدر wiki_cache_churn من 100→500 عميل — مرتبط بالتخزين المؤقت ومستقر، وبدون أي ضبط MALLOC_* (الضبط السابق MALLOC_ARENA_MAX=2 + عتبة التقليم متقاعد)؛ كما أن تنقيته الخلفية تعيد الذروة بعد الاندفاع بدلًا من تركها مثبتة
الذاكرة الإجمالية محدودةquery.maxIntermediateGlobal على مستوى الخادم + التوسعة المحسوبة على أساس التجاور تمسكان فيضان 2-hop في wiki_budget عند 1000 عميل دون OOM (RSS ~0.6 GB؛ الحارس يسقط ~60% من استعلامات المركز كأخطاء ميزانية قابلة لإعادة المحاولة)

كلتا مشكلتي الذاكرة اللتين كشفهما اختبار الحمل أصبحتا مغلقتين الآن؛ كلاهما موثّق في وثيقة اختبار الحمل.

الترخيص

مُرخَّص بموجب Apache License، الإصدار 2.0. انظر LICENSE للنص الكامل وNOTICE للإسناد. ما لم تنص صراحةً على خلاف ذلك، فإن أي مساهمة مقدَّمة عمدًا لإدراجها في هذا العمل، كما هو معرَّف في ترخيص Apache 2.0، ستُرخَّص كما هو مذكور أعلاه، دون أي شروط أو أحكام إضافية.

SPDX-License-Identifier: Apache-2.0

الفئات